Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -110,6 +110,7 @@ class ApiHandler(RequestHandler):
|
|||||||
|
|
||||||
if jsonp_callback:
|
if jsonp_callback:
|
||||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||||
|
self.set_header("Content-Type", "text/javascript")
|
||||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||||
self.redirect(result[1])
|
self.redirect(result[1])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'url',
|
'name': 'url',
|
||||||
'default': 'http://localhost:80/RPC2',
|
'default': 'http://localhost:80/RPC2',
|
||||||
|
'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
|
||||||
|
'or <strong>http://localhost:80/RPC2</strong>'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'username',
|
'name': 'username',
|
||||||
@@ -38,7 +40,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'directory',
|
'name': 'directory',
|
||||||
'type': 'directory',
|
'type': 'directory',
|
||||||
'description': 'Directory where rtorrent should download the files too.',
|
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'remove_complete',
|
'name': 'remove_complete',
|
||||||
@@ -48,14 +50,6 @@ config = [{
|
|||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
'description': 'Remove the torrent after it finishes seeding.',
|
'description': 'Remove the torrent after it finishes seeding.',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'name': 'append_label',
|
|
||||||
'label': 'Append Label',
|
|
||||||
'default': False,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Append label to download location. Requires you to set the download location above.',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'name': 'delete_files',
|
'name': 'delete_files',
|
||||||
'label': 'Remove files',
|
'label': 'Remove files',
|
||||||
@@ -64,6 +58,14 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Also remove the leftover files.',
|
'description': 'Also remove the leftover files.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'append_label',
|
||||||
|
'label': 'Append Label',
|
||||||
|
'default': False,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Append label to download location. Requires you to set the download location above.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'paused',
|
'name': 'paused',
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class rTorrent(Downloader):
|
|||||||
group.set_command()
|
group.set_command()
|
||||||
group.disable()
|
group.disable()
|
||||||
except MethodError, err:
|
except MethodError, err:
|
||||||
log.error('Unable to set group options: %s', err.message)
|
log.error('Unable to set group options: %s', err.msg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import collections
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -145,9 +146,9 @@ def getImdb(txt, check_inside = False, multiple = False):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def tryInt(s):
|
def tryInt(s, default=0):
|
||||||
try: return int(s)
|
try: return int(s)
|
||||||
except: return 0
|
except: return default
|
||||||
|
|
||||||
def tryFloat(s):
|
def tryFloat(s):
|
||||||
try:
|
try:
|
||||||
@@ -163,6 +164,11 @@ def natsortKey(s):
|
|||||||
def natcmp(a, b):
|
def natcmp(a, b):
|
||||||
return cmp(natsortKey(a), natsortKey(b))
|
return cmp(natsortKey(a), natsortKey(b))
|
||||||
|
|
||||||
|
def toIterable(value):
|
||||||
|
if isinstance(value, collections.Iterable):
|
||||||
|
return value
|
||||||
|
return [value]
|
||||||
|
|
||||||
def getTitle(library_dict):
|
def getTitle(library_dict):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,13 +1,44 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato import get_session
|
||||||
|
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.core.settings.model import Media
|
||||||
|
|
||||||
|
|
||||||
class MediaBase(Plugin):
|
class MediaBase(Plugin):
|
||||||
|
|
||||||
_type = None
|
_type = None
|
||||||
|
|
||||||
|
default_dict = {
|
||||||
|
'profile': {'types': {'quality': {}}},
|
||||||
|
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||||
|
'library': {'titles': {}, 'files':{}},
|
||||||
|
'files': {},
|
||||||
|
'status': {},
|
||||||
|
'category': {},
|
||||||
|
}
|
||||||
|
|
||||||
def initType(self):
|
def initType(self):
|
||||||
addEvent('media.types', self.getType)
|
addEvent('media.types', self.getType)
|
||||||
|
|
||||||
def getType(self):
|
def getType(self):
|
||||||
return self._type
|
return self._type
|
||||||
|
|
||||||
|
def createOnComplete(self, id):
|
||||||
|
|
||||||
|
def onComplete():
|
||||||
|
db = get_session()
|
||||||
|
media = db.query(Media).filter_by(id = id).first()
|
||||||
|
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.default_dict), on_complete = self.createNotifyFront(id))
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
|
return onComplete
|
||||||
|
|
||||||
|
def createNotifyFront(self, media_id):
|
||||||
|
|
||||||
|
def notifyFront():
|
||||||
|
db = get_session()
|
||||||
|
media = db.query(Media).filter_by(id = media_id).first()
|
||||||
|
fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict))
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
|
return notifyFront
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import MediaPlugin
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return MediaPlugin()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from couchpotato import get_session
|
||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||||
|
from couchpotato.core.helpers.variable import splitString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media import MediaBase
|
||||||
|
from couchpotato.core.settings.model import Media
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MediaPlugin(MediaBase):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
addApiView('media.refresh', self.refresh, docs = {
|
||||||
|
'desc': 'Refresh a any media type by ID',
|
||||||
|
'params': {
|
||||||
|
'id': {'desc': 'Movie, Show, Season or Episode ID(s) you want to refresh.', 'type': 'int (comma separated)'},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addEvent('app.load', self.addSingleRefresh)
|
||||||
|
|
||||||
|
def refresh(self, id = '', **kwargs):
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
for x in splitString(id):
|
||||||
|
media = db.query(Media).filter_by(id = x).first()
|
||||||
|
|
||||||
|
if media:
|
||||||
|
# Get current selected title
|
||||||
|
default_title = ''
|
||||||
|
for title in media.library.titles:
|
||||||
|
if title.default: default_title = title.title
|
||||||
|
|
||||||
|
fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True)
|
||||||
|
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def addSingleRefresh(self):
|
||||||
|
|
||||||
|
for media_type in fireEvent('media.types', merge = True):
|
||||||
|
addApiView('%s.refresh' % media_type, self.refresh)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import Search
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Search()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
|
from couchpotato.core.helpers.variable import mergeDicts
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Search(Plugin):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
addApiView('search', self.search, docs = {
|
||||||
|
'desc': 'Search the info in providers for a movie',
|
||||||
|
'params': {
|
||||||
|
'q': {'desc': 'The (partial) movie name you want to search for'},
|
||||||
|
'type': {'desc': 'Search for a specific media type. Leave empty to search all.'},
|
||||||
|
},
|
||||||
|
'return': {'type': 'object', 'example': """{
|
||||||
|
'success': True,
|
||||||
|
'movies': array,
|
||||||
|
'show': array,
|
||||||
|
etc
|
||||||
|
}"""}
|
||||||
|
})
|
||||||
|
|
||||||
|
addEvent('app.load', self.addSingleSearches)
|
||||||
|
|
||||||
|
def search(self, q = '', types = None, **kwargs):
|
||||||
|
|
||||||
|
# Make sure types is the correct instance
|
||||||
|
if isinstance(types, (str, unicode)):
|
||||||
|
types = [types]
|
||||||
|
elif isinstance(types, (list, tuple, set)):
|
||||||
|
types = list(types)
|
||||||
|
|
||||||
|
if not types:
|
||||||
|
result = fireEvent('info.search', q = q, merge = True)
|
||||||
|
else:
|
||||||
|
result = {}
|
||||||
|
for media_type in types:
|
||||||
|
result[media_type] = fireEvent('%s.search' % media_type)
|
||||||
|
|
||||||
|
return mergeDicts({
|
||||||
|
'success': True,
|
||||||
|
}, result)
|
||||||
|
|
||||||
|
def createSingleSearch(self, media_type):
|
||||||
|
|
||||||
|
def singleSearch(q, **kwargs):
|
||||||
|
return self.search(q, type = media_type, **kwargs)
|
||||||
|
|
||||||
|
return singleSearch
|
||||||
|
|
||||||
|
def addSingleSearches(self):
|
||||||
|
|
||||||
|
for media_type in fireEvent('media.types', merge = True):
|
||||||
|
addApiView('%s.search' % media_type, self.createSingleSearch(media_type))
|
||||||
+24
-24
@@ -129,13 +129,13 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result {
|
.media_result {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options {
|
.media_result .options {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -147,48 +147,48 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
||||||
}
|
}
|
||||||
.movie_result .options > .in_library_wanted {
|
.media_result .options > .in_library_wanted {
|
||||||
margin-top: -7px;
|
margin-top: -7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options > div {
|
.media_result .options > div {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options .thumbnail {
|
.media_result .options .thumbnail {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options select {
|
.media_result .options select {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.movie_result .options select[name=title] { width: 170px; }
|
.media_result .options select[name=title] { width: 170px; }
|
||||||
.movie_result .options select[name=profile] { width: 90px; }
|
.media_result .options select[name=profile] { width: 90px; }
|
||||||
.movie_result .options select[name=category] { width: 80px; }
|
.media_result .options select[name=category] { width: 80px; }
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
@media all and (max-width: 480px) {
|
||||||
|
|
||||||
.movie_result .options select[name=title] { width: 90px; }
|
.media_result .options select[name=title] { width: 90px; }
|
||||||
.movie_result .options select[name=profile] { width: 50px; }
|
.media_result .options select[name=profile] { width: 50px; }
|
||||||
.movie_result .options select[name=category] { width: 50px; }
|
.media_result .options select[name=category] { width: 50px; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options .button {
|
.media_result .options .button {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options .message {
|
.media_result .options .message {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .data {
|
.media_result .data {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -199,20 +199,20 @@
|
|||||||
border-top: 1px solid rgba(255,255,255, 0.08);
|
border-top: 1px solid rgba(255,255,255, 0.08);
|
||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
}
|
}
|
||||||
.movie_result .data.open {
|
.media_result .data.open {
|
||||||
left: 100% !important;
|
left: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result:last-child .data { border-bottom: 0; }
|
.media_result:last-child .data { border-bottom: 0; }
|
||||||
|
|
||||||
.movie_result .in_wanted, .movie_result .in_library {
|
.media_result .in_wanted, .media_result .in_library {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
left: 14px;
|
left: 14px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .thumbnail {
|
.media_result .thumbnail {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info {
|
.media_result .info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20%;
|
top: 20%;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info h2 {
|
.media_result .info h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info h2 .title {
|
.media_result .info h2 .title {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
width: 88%;
|
width: 88%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info h2 .year {
|
.media_result .info h2 .year {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search_form .mask,
|
.search_form .mask,
|
||||||
.movie_result .mask {
|
.media_result .mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
Block.Search = new Class({
|
||||||
|
|
||||||
|
Extends: BlockBase,
|
||||||
|
|
||||||
|
cache: {},
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var focus_timer = 0;
|
||||||
|
self.el = new Element('div.search_form').adopt(
|
||||||
|
new Element('div.input').adopt(
|
||||||
|
self.input = new Element('input', {
|
||||||
|
'placeholder': 'Search & add a new media',
|
||||||
|
'events': {
|
||||||
|
'keyup': self.keyup.bind(self),
|
||||||
|
'focus': function(){
|
||||||
|
if(focus_timer) clearTimeout(focus_timer);
|
||||||
|
self.el.addClass('focused')
|
||||||
|
if(this.get('value'))
|
||||||
|
self.hideResults(false)
|
||||||
|
},
|
||||||
|
'blur': function(){
|
||||||
|
focus_timer = (function(){
|
||||||
|
self.el.removeClass('focused')
|
||||||
|
}).delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('a.icon2', {
|
||||||
|
'events': {
|
||||||
|
'click': self.clear.bind(self),
|
||||||
|
'touchend': self.clear.bind(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
self.result_container = new Element('div.results_container', {
|
||||||
|
'tween': {
|
||||||
|
'duration': 200
|
||||||
|
},
|
||||||
|
'events': {
|
||||||
|
'mousewheel': function(e){
|
||||||
|
(e).stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).adopt(
|
||||||
|
self.results = new Element('div.results')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
if(self.last_q === ''){
|
||||||
|
self.input.blur()
|
||||||
|
self.last_q = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
self.last_q = '';
|
||||||
|
self.input.set('value', '');
|
||||||
|
self.input.focus()
|
||||||
|
|
||||||
|
self.media = {}
|
||||||
|
self.results.empty()
|
||||||
|
self.el.removeClass('filled')
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hideResults: function(bool){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(self.hidden == bool) return;
|
||||||
|
|
||||||
|
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
||||||
|
|
||||||
|
if(bool){
|
||||||
|
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
||||||
|
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
History.addEvent('change', self.hideResults.bind(self, !bool));
|
||||||
|
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hidden = bool;
|
||||||
|
},
|
||||||
|
|
||||||
|
keyup: function(e){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||||
|
|
||||||
|
if(self.q() != self.last_q){
|
||||||
|
if(self.api_request && self.api_request.isRunning())
|
||||||
|
self.api_request.cancel();
|
||||||
|
|
||||||
|
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||||
|
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!self.q()){
|
||||||
|
self.hideResults(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.list()
|
||||||
|
},
|
||||||
|
|
||||||
|
list: function(){
|
||||||
|
var self = this,
|
||||||
|
q = self.q(),
|
||||||
|
cache = self.cache[q];
|
||||||
|
|
||||||
|
self.hideResults(false);
|
||||||
|
|
||||||
|
if(!cache){
|
||||||
|
self.mask.fade('in');
|
||||||
|
|
||||||
|
if(!self.spinner)
|
||||||
|
self.spinner = createSpinner(self.mask);
|
||||||
|
|
||||||
|
self.api_request = Api.request('search', {
|
||||||
|
'data': {
|
||||||
|
'q': q
|
||||||
|
},
|
||||||
|
'onComplete': self.fill.bind(self, q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
self.fill(q, cache)
|
||||||
|
|
||||||
|
self.last_q = q;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
fill: function(q, json){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.cache[q] = json
|
||||||
|
|
||||||
|
self.media = {}
|
||||||
|
self.results.empty()
|
||||||
|
|
||||||
|
Object.each(json, function(media, type){
|
||||||
|
if(typeOf(media) == 'array'){
|
||||||
|
Object.each(media, function(m){
|
||||||
|
|
||||||
|
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
|
||||||
|
$(m).inject(self.results)
|
||||||
|
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
||||||
|
|
||||||
|
if(q == m.imdb)
|
||||||
|
m.showOptions()
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate result heights
|
||||||
|
var w = window.getSize(),
|
||||||
|
rc = self.result_container.getCoordinates();
|
||||||
|
|
||||||
|
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
||||||
|
self.mask.fade('out')
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
loading: function(bool){
|
||||||
|
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||||
|
},
|
||||||
|
|
||||||
|
q: function(){
|
||||||
|
return this.input.get('value').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -2,12 +2,13 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||||
from couchpotato.core.helpers.variable import md5, getTitle
|
from couchpotato.core.helpers.variable import md5, getTitle, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
from couchpotato.core.settings.model import Media, Release, ReleaseInfo
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from inspect import ismethod, isfunction
|
from inspect import ismethod, isfunction
|
||||||
|
from sqlalchemy.exc import InterfaceError
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -23,7 +24,10 @@ class Searcher(SearcherBase):
|
|||||||
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
||||||
addEvent('searcher.correct_year', self.correctYear)
|
addEvent('searcher.correct_year', self.correctYear)
|
||||||
addEvent('searcher.correct_name', self.correctName)
|
addEvent('searcher.correct_name', self.correctName)
|
||||||
|
addEvent('searcher.correct_words', self.correctWords)
|
||||||
addEvent('searcher.download', self.download)
|
addEvent('searcher.download', self.download)
|
||||||
|
addEvent('searcher.search', self.search)
|
||||||
|
addEvent('searcher.create_releases', self.createReleases)
|
||||||
|
|
||||||
addApiView('searcher.full_search', self.searchAllView, docs = {
|
addApiView('searcher.full_search', self.searchAllView, docs = {
|
||||||
'desc': 'Starts a full search for all media',
|
'desc': 'Starts a full search for all media',
|
||||||
@@ -79,7 +83,6 @@ class Searcher(SearcherBase):
|
|||||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||||
if rls:
|
if rls:
|
||||||
renamer_enabled = Env.setting('enabled', 'renamer')
|
renamer_enabled = Env.setting('enabled', 'renamer')
|
||||||
fireEvent('release.update_status', rls.id, status = done_status if not renamer_enabled else snatched_status, single = True)
|
|
||||||
|
|
||||||
# Save download-id info if returned
|
# Save download-id info if returned
|
||||||
if isinstance(download_result, dict):
|
if isinstance(download_result, dict):
|
||||||
@@ -102,14 +105,20 @@ class Searcher(SearcherBase):
|
|||||||
if movie['status_id'] == active_status.get('id'):
|
if movie['status_id'] == active_status.get('id'):
|
||||||
for profile_type in movie['profile']['types']:
|
for profile_type in movie['profile']['types']:
|
||||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||||
# Mark movie done
|
|
||||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
|
||||||
|
# Mark release done
|
||||||
|
fireEvent('release.update_status', rls.id, status = done_status, single = True)
|
||||||
|
|
||||||
|
# Mark movie done
|
||||||
|
mvie = db.query(Media).filter_by(id = movie['id']).first()
|
||||||
mvie.status_id = done_status.get('id')
|
mvie.status_id = done_status.get('id')
|
||||||
mvie.last_edit = int(time.time())
|
mvie.last_edit = int(time.time())
|
||||||
db.commit()
|
db.commit()
|
||||||
except:
|
except:
|
||||||
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
||||||
|
else:
|
||||||
|
fireEvent('release.update_status', rls.id, status = snatched_status, single = True)
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
||||||
@@ -120,6 +129,75 @@ class Searcher(SearcherBase):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def search(self, protocols, media, quality):
|
||||||
|
results = []
|
||||||
|
|
||||||
|
search_type = None
|
||||||
|
if media['type'] == 'movie':
|
||||||
|
search_type = 'movie'
|
||||||
|
elif media['type'] in ['show', 'season', 'episode']:
|
||||||
|
search_type = 'show'
|
||||||
|
|
||||||
|
for search_protocol in protocols:
|
||||||
|
protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, search_type), media, quality, merge = True)
|
||||||
|
if protocol_results:
|
||||||
|
results += protocol_results
|
||||||
|
|
||||||
|
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
||||||
|
|
||||||
|
download_preference = self.conf('preferred_method', section = 'searcher')
|
||||||
|
if download_preference != 'both':
|
||||||
|
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
||||||
|
|
||||||
|
return sorted_results
|
||||||
|
|
||||||
|
def createReleases(self, search_results, media, quality_type):
|
||||||
|
|
||||||
|
available_status = fireEvent('status.get', ['available'], single = True)
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
found_releases = []
|
||||||
|
|
||||||
|
for rel in search_results:
|
||||||
|
|
||||||
|
nzb_identifier = md5(rel['url'])
|
||||||
|
found_releases.append(nzb_identifier)
|
||||||
|
|
||||||
|
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
|
||||||
|
if not rls:
|
||||||
|
rls = Release(
|
||||||
|
identifier = nzb_identifier,
|
||||||
|
movie_id = media.get('id'),
|
||||||
|
#media_id = media.get('id'),
|
||||||
|
quality_id = quality_type.get('quality_id'),
|
||||||
|
status_id = available_status.get('id')
|
||||||
|
)
|
||||||
|
db.add(rls)
|
||||||
|
else:
|
||||||
|
[db.delete(old_info) for old_info in rls.info]
|
||||||
|
rls.last_edit = int(time.time())
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
for info in rel:
|
||||||
|
try:
|
||||||
|
if not isinstance(rel[info], (str, unicode, int, long, float)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rls_info = ReleaseInfo(
|
||||||
|
identifier = info,
|
||||||
|
value = toUnicode(rel[info])
|
||||||
|
)
|
||||||
|
rls.info.append(rls_info)
|
||||||
|
except InterfaceError:
|
||||||
|
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
rel['status_id'] = rls.status_id
|
||||||
|
|
||||||
|
return found_releases
|
||||||
|
|
||||||
def getSearchProtocols(self):
|
def getSearchProtocols(self):
|
||||||
|
|
||||||
download_protocols = fireEvent('download.enabled_protocols', merge = True)
|
download_protocols = fireEvent('download.enabled_protocols', merge = True)
|
||||||
@@ -224,5 +302,49 @@ class Searcher(SearcherBase):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def correctWords(self, rel_name, media):
|
||||||
|
media_title = fireEvent('searcher.get_search_title', media, single = True)
|
||||||
|
media_words = re.split('\W+', simplifyString(media_title))
|
||||||
|
|
||||||
|
rel_name = simplifyString(rel_name)
|
||||||
|
rel_words = re.split('\W+', rel_name)
|
||||||
|
|
||||||
|
# Make sure it has required words
|
||||||
|
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||||
|
try: required_words = list(set(required_words + splitString(media['category']['required'].lower())))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
req_match = 0
|
||||||
|
for req_set in required_words:
|
||||||
|
req = splitString(req_set, '&')
|
||||||
|
req_match += len(list(set(rel_words) & set(req))) == len(req)
|
||||||
|
|
||||||
|
if len(required_words) > 0 and req_match == 0:
|
||||||
|
log.info2('Wrong: Required word missing: %s', rel_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ignore releases
|
||||||
|
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
||||||
|
try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower())))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
ignored_match = 0
|
||||||
|
for ignored_set in ignored_words:
|
||||||
|
ignored = splitString(ignored_set, '&')
|
||||||
|
ignored_match += len(list(set(rel_words) & set(ignored))) == len(ignored)
|
||||||
|
|
||||||
|
if len(ignored_words) > 0 and ignored_match:
|
||||||
|
log.info2("Wrong: '%s' contains 'ignored words'", rel_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ignore porn stuff
|
||||||
|
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
||||||
|
pron_words = list(set(rel_words) & set(pron_tags) - set(media_words))
|
||||||
|
if pron_words:
|
||||||
|
log.info('Wrong: %s, probably pr0n', rel_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
class SearchSetupError(Exception):
|
class SearchSetupError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
|||||||
mergeDicts
|
mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.media.movie import MovieTypeBase
|
from couchpotato.core.media.movie import MovieTypeBase
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
|
from couchpotato.core.settings.model import Library, LibraryTitle, Media, \
|
||||||
Release
|
Release
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
@@ -19,14 +19,7 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class MovieBase(MovieTypeBase):
|
class MovieBase(MovieTypeBase):
|
||||||
|
|
||||||
default_dict = {
|
_type = 'movie'
|
||||||
'profile': {'types': {'quality': {}}},
|
|
||||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
|
||||||
'library': {'titles': {}, 'files':{}},
|
|
||||||
'files': {},
|
|
||||||
'status': {},
|
|
||||||
'category': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -34,17 +27,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
super(MovieBase, self).__init__()
|
super(MovieBase, self).__init__()
|
||||||
self.initType()
|
self.initType()
|
||||||
|
|
||||||
addApiView('movie.search', self.search, docs = {
|
|
||||||
'desc': 'Search the movie providers for a movie',
|
|
||||||
'params': {
|
|
||||||
'q': {'desc': 'The (partial) movie name you want to search for'},
|
|
||||||
},
|
|
||||||
'return': {'type': 'object', 'example': """{
|
|
||||||
'success': True,
|
|
||||||
'empty': bool, any movies returned or not,
|
|
||||||
'movies': array, movies found,
|
|
||||||
}"""}
|
|
||||||
})
|
|
||||||
addApiView('movie.list', self.listView, docs = {
|
addApiView('movie.list', self.listView, docs = {
|
||||||
'desc': 'List movies in wanted list',
|
'desc': 'List movies in wanted list',
|
||||||
'params': {
|
'params': {
|
||||||
@@ -66,12 +48,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
'id': {'desc': 'The id of the movie'},
|
'id': {'desc': 'The id of the movie'},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
addApiView('movie.refresh', self.refresh, docs = {
|
|
||||||
'desc': 'Refresh a movie by id',
|
|
||||||
'params': {
|
|
||||||
'id': {'desc': 'Movie ID(s) you want to refresh.', 'type': 'int (comma separated)'},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
addApiView('movie.available_chars', self.charView)
|
addApiView('movie.available_chars', self.charView)
|
||||||
addApiView('movie.add', self.addView, docs = {
|
addApiView('movie.add', self.addView, docs = {
|
||||||
'desc': 'Add new movie to the wanted list',
|
'desc': 'Add new movie to the wanted list',
|
||||||
@@ -103,34 +79,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
addEvent('movie.list', self.list)
|
addEvent('movie.list', self.list)
|
||||||
addEvent('movie.restatus', self.restatus)
|
addEvent('movie.restatus', self.restatus)
|
||||||
|
|
||||||
# Clean releases that didn't have activity in the last week
|
|
||||||
addEvent('app.load', self.cleanReleases)
|
|
||||||
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
|
|
||||||
|
|
||||||
def cleanReleases(self):
|
|
||||||
|
|
||||||
log.debug('Removing releases from dashboard')
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
week = 262080
|
|
||||||
|
|
||||||
done_status, available_status, snatched_status = \
|
|
||||||
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
|
|
||||||
|
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
# get movies last_edit more than a week ago
|
|
||||||
movies = db.query(Movie) \
|
|
||||||
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
|
||||||
.all()
|
|
||||||
|
|
||||||
for movie in movies:
|
|
||||||
for rel in movie.releases:
|
|
||||||
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
|
||||||
fireEvent('release.delete', id = rel.id, single = True)
|
|
||||||
|
|
||||||
db.expire_all()
|
|
||||||
|
|
||||||
def getView(self, id = None, **kwargs):
|
def getView(self, id = None, **kwargs):
|
||||||
|
|
||||||
movie = self.get(id) if id else None
|
movie = self.get(id) if id else None
|
||||||
@@ -147,9 +95,9 @@ class MovieBase(MovieTypeBase):
|
|||||||
imdb_id = getImdb(str(movie_id))
|
imdb_id = getImdb(str(movie_id))
|
||||||
|
|
||||||
if imdb_id:
|
if imdb_id:
|
||||||
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
||||||
else:
|
else:
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Media).filter_by(id = movie_id).first()
|
||||||
|
|
||||||
results = None
|
results = None
|
||||||
if m:
|
if m:
|
||||||
@@ -169,20 +117,20 @@ class MovieBase(MovieTypeBase):
|
|||||||
release_status = [release_status]
|
release_status = [release_status]
|
||||||
|
|
||||||
# query movie ids
|
# query movie ids
|
||||||
q = db.query(Movie) \
|
q = db.query(Media) \
|
||||||
.with_entities(Movie.id) \
|
.with_entities(Media.id) \
|
||||||
.group_by(Movie.id)
|
.group_by(Media.id)
|
||||||
|
|
||||||
# Filter on movie status
|
# Filter on movie status
|
||||||
if status and len(status) > 0:
|
if status and len(status) > 0:
|
||||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
q = q.filter(Movie.status_id.in_(statuses))
|
q = q.filter(Media.status_id.in_(statuses))
|
||||||
|
|
||||||
# Filter on release status
|
# Filter on release status
|
||||||
if release_status and len(release_status) > 0:
|
if release_status and len(release_status) > 0:
|
||||||
q = q.join(Movie.releases)
|
q = q.join(Media.releases)
|
||||||
|
|
||||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
@@ -191,7 +139,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
|
|
||||||
# Only join when searching / ordering
|
# Only join when searching / ordering
|
||||||
if starts_with or search or order != 'release_order':
|
if starts_with or search or order != 'release_order':
|
||||||
q = q.join(Movie.library, Library.titles) \
|
q = q.join(Media.library, Library.titles) \
|
||||||
.filter(LibraryTitle.default == True)
|
.filter(LibraryTitle.default == True)
|
||||||
|
|
||||||
# Add search filters
|
# Add search filters
|
||||||
@@ -242,13 +190,13 @@ class MovieBase(MovieTypeBase):
|
|||||||
releases_count[release.movie_id] += 1
|
releases_count[release.movie_id] += 1
|
||||||
|
|
||||||
# Get main movie data
|
# Get main movie data
|
||||||
q2 = db.query(Movie) \
|
q2 = db.query(Media) \
|
||||||
.options(joinedload_all('library.titles')) \
|
.options(joinedload_all('library.titles')) \
|
||||||
.options(joinedload_all('library.files')) \
|
.options(joinedload_all('library.files')) \
|
||||||
.options(joinedload_all('status')) \
|
.options(joinedload_all('status')) \
|
||||||
.options(joinedload_all('files'))
|
.options(joinedload_all('files'))
|
||||||
|
|
||||||
q2 = q2.filter(Movie.id.in_(movie_ids))
|
q2 = q2.filter(Media.id.in_(movie_ids))
|
||||||
|
|
||||||
results = q2.all()
|
results = q2.all()
|
||||||
|
|
||||||
@@ -291,14 +239,14 @@ class MovieBase(MovieTypeBase):
|
|||||||
if release_status and not isinstance(release_status, (list, tuple)):
|
if release_status and not isinstance(release_status, (list, tuple)):
|
||||||
release_status = [release_status]
|
release_status = [release_status]
|
||||||
|
|
||||||
q = db.query(Movie)
|
q = db.query(Media)
|
||||||
|
|
||||||
# Filter on movie status
|
# Filter on movie status
|
||||||
if status and len(status) > 0:
|
if status and len(status) > 0:
|
||||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
q = q.filter(Movie.status_id.in_(statuses))
|
q = q.filter(Media.status_id.in_(statuses))
|
||||||
|
|
||||||
# Filter on release status
|
# Filter on release status
|
||||||
if release_status and len(release_status) > 0:
|
if release_status and len(release_status) > 0:
|
||||||
@@ -306,7 +254,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
q = q.join(Movie.releases) \
|
q = q.join(Media.releases) \
|
||||||
.filter(Release.status_id.in_(statuses))
|
.filter(Release.status_id.in_(statuses))
|
||||||
|
|
||||||
q = q.join(Library, LibraryTitle) \
|
q = q.join(Library, LibraryTitle) \
|
||||||
@@ -367,28 +315,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
'chars': chars,
|
'chars': chars,
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh(self, id = '', **kwargs):
|
|
||||||
|
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
for x in splitString(id):
|
|
||||||
movie = db.query(Movie).filter_by(id = x).first()
|
|
||||||
|
|
||||||
if movie:
|
|
||||||
|
|
||||||
# Get current selected title
|
|
||||||
default_title = ''
|
|
||||||
for title in movie.library.titles:
|
|
||||||
if title.default: default_title = title.title
|
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
|
||||||
fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
|
||||||
|
|
||||||
db.expire_all()
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def search(self, q = '', **kwargs):
|
def search(self, q = '', **kwargs):
|
||||||
|
|
||||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||||
@@ -438,12 +364,12 @@ class MovieBase(MovieTypeBase):
|
|||||||
cat_id = params.get('category_id')
|
cat_id = params.get('category_id')
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
m = db.query(Media).filter_by(library_id = library.get('id')).first()
|
||||||
added = True
|
added = True
|
||||||
do_search = False
|
do_search = False
|
||||||
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||||
if not m:
|
if not m:
|
||||||
m = Movie(
|
m = Media(
|
||||||
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_id if status_id else status_active.get('id'),
|
status_id = status_id if status_id else status_active.get('id'),
|
||||||
@@ -500,15 +426,12 @@ class MovieBase(MovieTypeBase):
|
|||||||
db.expire_all()
|
db.expire_all()
|
||||||
return movie_dict
|
return movie_dict
|
||||||
|
|
||||||
|
|
||||||
def addView(self, **kwargs):
|
def addView(self, **kwargs):
|
||||||
|
add_dict = self.add(params = kwargs)
|
||||||
movie_dict = self.add(params = kwargs)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True if add_dict else False,
|
||||||
'added': True if movie_dict else False,
|
'movie': add_dict,
|
||||||
'movie': movie_dict,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def edit(self, id = '', **kwargs):
|
def edit(self, id = '', **kwargs):
|
||||||
@@ -520,7 +443,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
ids = splitString(id)
|
ids = splitString(id)
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
|
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Media).filter_by(id = movie_id).first()
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -567,7 +490,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
movie = db.query(Media).filter_by(id = movie_id).first()
|
||||||
if movie:
|
if movie:
|
||||||
deleted = False
|
deleted = False
|
||||||
if delete_from == 'all':
|
if delete_from == 'all':
|
||||||
@@ -617,7 +540,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Media).filter_by(id = movie_id).first()
|
||||||
if not m or len(m.library.titles) == 0:
|
if not m or len(m.library.titles) == 0:
|
||||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||||
return False
|
return False
|
||||||
@@ -638,24 +561,3 @@ class MovieBase(MovieTypeBase):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def createOnComplete(self, movie_id):
|
|
||||||
|
|
||||||
def onComplete():
|
|
||||||
db = get_session()
|
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
|
||||||
fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
|
||||||
db.expire_all()
|
|
||||||
|
|
||||||
return onComplete
|
|
||||||
|
|
||||||
|
|
||||||
def createNotifyFront(self, movie_id):
|
|
||||||
|
|
||||||
def notifyFront():
|
|
||||||
db = get_session()
|
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
|
||||||
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
|
||||||
db.expire_all()
|
|
||||||
|
|
||||||
return notifyFront
|
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ var MovieList = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
var ids = self.getSelectedMovies()
|
var ids = self.getSelectedMovies()
|
||||||
|
|
||||||
Api.request('movie.refresh', {
|
Api.request('media.refresh', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': ids.join(','),
|
'id': ids.join(','),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -706,7 +706,7 @@ MA.Refresh = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
(e).preventDefault();
|
(e).preventDefault();
|
||||||
|
|
||||||
Api.request('movie.refresh', {
|
Api.request('media.refresh', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': self.movie.get('id')
|
'id': self.movie.get('id')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,7 +436,7 @@
|
|||||||
}
|
}
|
||||||
.movies .data .quality .seeding { background-color: #0a6819; }
|
.movies .data .quality .seeding { background-color: #0a6819; }
|
||||||
.movies .data .quality .finish {
|
.movies .data .quality .finish {
|
||||||
background-image: url('../images/sprite.png');
|
background-image: url('../../images/sprite.png');
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 0 2px;
|
background-position: 0 2px;
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
@@ -992,7 +992,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.movies .empty_wanted {
|
.movies .empty_wanted {
|
||||||
background-image: url('../images/emptylist.png');
|
background-image: url('../../images/emptylist.png');
|
||||||
background-position: 80% 0;
|
background-position: 80% 0;
|
||||||
height: 750px;
|
height: 750px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,189 +1,4 @@
|
|||||||
Block.Search = new Class({
|
Block.Search.MovieItem = new Class({
|
||||||
|
|
||||||
Extends: BlockBase,
|
|
||||||
|
|
||||||
cache: {},
|
|
||||||
|
|
||||||
create: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var focus_timer = 0;
|
|
||||||
self.el = new Element('div.search_form').adopt(
|
|
||||||
new Element('div.input').adopt(
|
|
||||||
self.input = new Element('input', {
|
|
||||||
'placeholder': 'Search & add a new movie',
|
|
||||||
'events': {
|
|
||||||
'keyup': self.keyup.bind(self),
|
|
||||||
'focus': function(){
|
|
||||||
if(focus_timer) clearTimeout(focus_timer);
|
|
||||||
self.el.addClass('focused')
|
|
||||||
if(this.get('value'))
|
|
||||||
self.hideResults(false)
|
|
||||||
},
|
|
||||||
'blur': function(){
|
|
||||||
focus_timer = (function(){
|
|
||||||
self.el.removeClass('focused')
|
|
||||||
}).delay(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new Element('a.icon2', {
|
|
||||||
'events': {
|
|
||||||
'click': self.clear.bind(self),
|
|
||||||
'touchend': self.clear.bind(self)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
),
|
|
||||||
self.result_container = new Element('div.results_container', {
|
|
||||||
'tween': {
|
|
||||||
'duration': 200
|
|
||||||
},
|
|
||||||
'events': {
|
|
||||||
'mousewheel': function(e){
|
|
||||||
(e).stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).adopt(
|
|
||||||
self.results = new Element('div.results')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
clear: function(e){
|
|
||||||
var self = this;
|
|
||||||
(e).preventDefault();
|
|
||||||
|
|
||||||
if(self.last_q === ''){
|
|
||||||
self.input.blur()
|
|
||||||
self.last_q = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
self.last_q = '';
|
|
||||||
self.input.set('value', '');
|
|
||||||
self.input.focus()
|
|
||||||
|
|
||||||
self.movies = []
|
|
||||||
self.results.empty()
|
|
||||||
self.el.removeClass('filled')
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hideResults: function(bool){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(self.hidden == bool) return;
|
|
||||||
|
|
||||||
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
|
||||||
|
|
||||||
if(bool){
|
|
||||||
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
|
||||||
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
History.addEvent('change', self.hideResults.bind(self, !bool));
|
|
||||||
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.hidden = bool;
|
|
||||||
},
|
|
||||||
|
|
||||||
keyup: function(e){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
|
||||||
|
|
||||||
if(self.q() != self.last_q){
|
|
||||||
if(self.api_request && self.api_request.isRunning())
|
|
||||||
self.api_request.cancel();
|
|
||||||
|
|
||||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
|
||||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(!self.q()){
|
|
||||||
self.hideResults(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.list()
|
|
||||||
},
|
|
||||||
|
|
||||||
list: function(){
|
|
||||||
var self = this,
|
|
||||||
q = self.q(),
|
|
||||||
cache = self.cache[q];
|
|
||||||
|
|
||||||
self.hideResults(false);
|
|
||||||
|
|
||||||
if(!cache){
|
|
||||||
self.mask.fade('in');
|
|
||||||
|
|
||||||
if(!self.spinner)
|
|
||||||
self.spinner = createSpinner(self.mask);
|
|
||||||
|
|
||||||
self.api_request = Api.request('movie.search', {
|
|
||||||
'data': {
|
|
||||||
'q': q
|
|
||||||
},
|
|
||||||
'onComplete': self.fill.bind(self, q)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else
|
|
||||||
self.fill(q, cache)
|
|
||||||
|
|
||||||
self.last_q = q;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
fill: function(q, json){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self.cache[q] = json
|
|
||||||
|
|
||||||
self.movies = {}
|
|
||||||
self.results.empty()
|
|
||||||
|
|
||||||
Object.each(json.movies, function(movie){
|
|
||||||
|
|
||||||
var m = new Block.Search.Item(movie);
|
|
||||||
$(m).inject(self.results)
|
|
||||||
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
|
||||||
|
|
||||||
if(q == movie.imdb)
|
|
||||||
m.showOptions()
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate result heights
|
|
||||||
var w = window.getSize(),
|
|
||||||
rc = self.result_container.getCoordinates();
|
|
||||||
|
|
||||||
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
|
||||||
self.mask.fade('out')
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
loading: function(bool){
|
|
||||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
|
||||||
},
|
|
||||||
|
|
||||||
q: function(){
|
|
||||||
return this.input.get('value').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Block.Search.Item = new Class({
|
|
||||||
|
|
||||||
Implements: [Options, Events],
|
Implements: [Options, Events],
|
||||||
|
|
||||||
@@ -201,7 +16,7 @@ Block.Search.Item = new Class({
|
|||||||
var self = this,
|
var self = this,
|
||||||
info = self.info;
|
info = self.info;
|
||||||
|
|
||||||
self.el = new Element('div.movie_result', {
|
self.el = new Element('div.media_result', {
|
||||||
'id': info.imdb
|
'id': info.imdb
|
||||||
}).adopt(
|
}).adopt(
|
||||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
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, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
|
from couchpotato.core.helpers.encoding import simplifyString
|
||||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb
|
||||||
possibleTitles, getImdb
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||||
from couchpotato.core.media.movie import MovieTypeBase
|
from couchpotato.core.media.movie import MovieTypeBase
|
||||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
from couchpotato.core.settings.model import Media, Release
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from sqlalchemy.exc import InterfaceError
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -29,9 +27,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
addEvent('movie.searcher.all', self.searchAll)
|
addEvent('movie.searcher.all', self.searchAll)
|
||||||
addEvent('movie.searcher.all_view', self.searchAllView)
|
addEvent('movie.searcher.all_view', self.searchAllView)
|
||||||
addEvent('movie.searcher.single', self.single)
|
addEvent('movie.searcher.single', self.single)
|
||||||
addEvent('movie.searcher.correct_movie', self.correctMovie)
|
|
||||||
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
|
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
|
||||||
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
|
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
|
||||||
|
addEvent('searcher.correct_release', self.correctRelease)
|
||||||
|
addEvent('searcher.get_search_title', self.getSearchTitle)
|
||||||
|
|
||||||
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
|
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
|
||||||
'desc': 'Marks the snatched results as ignored and try the next best release',
|
'desc': 'Marks the snatched results as ignored and try the next best release',
|
||||||
@@ -74,8 +73,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
movies = db.query(Movie).filter(
|
movies = db.query(Media).filter(
|
||||||
Movie.status.has(identifier = 'active')
|
Media.status.has(identifier = 'active')
|
||||||
).all()
|
).all()
|
||||||
random.shuffle(movies)
|
random.shuffle(movies)
|
||||||
|
|
||||||
@@ -117,6 +116,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
|
|
||||||
def single(self, movie, search_protocols = None, manual = False):
|
def single(self, movie, search_protocols = None, manual = False):
|
||||||
|
|
||||||
|
# movies don't contain 'type' yet, so just set to default here
|
||||||
|
if not movie.has_key('type'):
|
||||||
|
movie['type'] = 'movie'
|
||||||
|
|
||||||
# Find out search type
|
# Find out search type
|
||||||
try:
|
try:
|
||||||
if not search_protocols:
|
if not search_protocols:
|
||||||
@@ -167,64 +170,18 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
||||||
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||||
|
|
||||||
results = []
|
results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
|
||||||
for search_protocol in search_protocols:
|
if len(results) == 0:
|
||||||
protocol_results = fireEvent('provider.search.%s.movie' % search_protocol, movie, quality, merge = True)
|
|
||||||
if protocol_results:
|
|
||||||
results += protocol_results
|
|
||||||
|
|
||||||
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
|
||||||
if len(sorted_results) == 0:
|
|
||||||
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
||||||
|
|
||||||
download_preference = self.conf('preferred_method', section = 'searcher')
|
|
||||||
if download_preference != 'both':
|
|
||||||
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
|
||||||
|
|
||||||
# Check if movie isn't deleted while searching
|
# Check if movie isn't deleted while searching
|
||||||
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
if not db.query(Media).filter_by(id = movie.get('id')).first():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Add them to this movie releases list
|
# Add them to this movie releases list
|
||||||
for nzb in sorted_results:
|
found_releases += fireEvent('searcher.create_releases', results, movie, quality_type, single = True)
|
||||||
|
|
||||||
nzb_identifier = md5(nzb['url'])
|
for nzb in results:
|
||||||
found_releases.append(nzb_identifier)
|
|
||||||
|
|
||||||
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
|
|
||||||
if not rls:
|
|
||||||
rls = Release(
|
|
||||||
identifier = nzb_identifier,
|
|
||||||
movie_id = movie.get('id'),
|
|
||||||
quality_id = quality_type.get('quality_id'),
|
|
||||||
status_id = available_status.get('id')
|
|
||||||
)
|
|
||||||
db.add(rls)
|
|
||||||
else:
|
|
||||||
[db.delete(old_info) for old_info in rls.info]
|
|
||||||
rls.last_edit = int(time.time())
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
for info in nzb:
|
|
||||||
try:
|
|
||||||
if not isinstance(nzb[info], (str, unicode, int, long, float)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
rls_info = ReleaseInfo(
|
|
||||||
identifier = info,
|
|
||||||
value = toUnicode(nzb[info])
|
|
||||||
)
|
|
||||||
rls.info.append(rls_info)
|
|
||||||
except InterfaceError:
|
|
||||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
nzb['status_id'] = rls.status_id
|
|
||||||
|
|
||||||
|
|
||||||
for nzb in sorted_results:
|
|
||||||
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
||||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||||
continue
|
continue
|
||||||
@@ -265,7 +222,11 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
|
def correctRelease(self, nzb = None, media = None, quality = None, **kwargs):
|
||||||
|
|
||||||
|
if media.get('type') != 'movie': return
|
||||||
|
|
||||||
|
media_title = fireEvent('searcher.get_search_title', media, single = True)
|
||||||
|
|
||||||
imdb_results = kwargs.get('imdb_results', False)
|
imdb_results = kwargs.get('imdb_results', False)
|
||||||
retention = Env.setting('retention', section = 'nzb')
|
retention = Env.setting('retention', section = 'nzb')
|
||||||
@@ -274,50 +235,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
movie_name = getTitle(movie['library'])
|
# Check for required and ignored words
|
||||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
if not fireEvent('searcher.correct_words', nzb['name'], media, single = True):
|
||||||
nzb_name = simplifyString(nzb['name'])
|
|
||||||
nzb_words = re.split('\W+', nzb_name)
|
|
||||||
|
|
||||||
# Make sure it has required words
|
|
||||||
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
|
||||||
try: required_words = list(set(required_words + splitString(movie['category']['required'].lower())))
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
req_match = 0
|
|
||||||
for req_set in required_words:
|
|
||||||
req = splitString(req_set, '&')
|
|
||||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
|
||||||
|
|
||||||
if len(required_words) > 0 and req_match == 0:
|
|
||||||
log.info2('Wrong: Required word missing: %s', nzb['name'])
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ignore releases
|
|
||||||
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
|
||||||
try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower())))
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
ignored_match = 0
|
|
||||||
for ignored_set in ignored_words:
|
|
||||||
ignored = splitString(ignored_set, '&')
|
|
||||||
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
|
|
||||||
|
|
||||||
if len(ignored_words) > 0 and ignored_match:
|
|
||||||
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ignore porn stuff
|
|
||||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
|
||||||
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
|
||||||
if pron_words:
|
|
||||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
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 fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True):
|
if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['library']['year'], preferred_quality = preferred_quality, single = True):
|
||||||
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -347,23 +272,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Check if nzb contains imdb link
|
# Check if nzb contains imdb link
|
||||||
if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
|
if getImdb(nzb.get('description', '')) == media['library']['identifier']:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for raw_title in movie['library']['titles']:
|
for raw_title in media['library']['titles']:
|
||||||
for movie_title in possibleTitles(raw_title['title']):
|
for movie_title in possibleTitles(raw_title['title']):
|
||||||
movie_words = re.split('\W+', simplifyString(movie_title))
|
movie_words = re.split('\W+', simplifyString(movie_title))
|
||||||
|
|
||||||
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
|
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
|
||||||
# if no IMDB link, at least check year range 1
|
# if no IMDB link, at least check year range 1
|
||||||
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True):
|
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 1, single = True):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# if no IMDB link, at least check year
|
# if no IMDB link, at least check year
|
||||||
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True):
|
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 0, single = True):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
|
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['library']['year']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def couldBeReleased(self, is_pre_release, dates, year = None):
|
def couldBeReleased(self, is_pre_release, dates, year = None):
|
||||||
@@ -434,5 +359,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getSearchTitle(self, media):
|
||||||
|
if media['type'] == 'movie':
|
||||||
|
return getTitle(media['library'])
|
||||||
|
|
||||||
class SearchSetupError(Exception):
|
class SearchSetupError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
|
|||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.variable import splitString
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie, Library
|
from couchpotato.core.settings.model import Media, Library
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import or_
|
from sqlalchemy.sql.expression import or_
|
||||||
@@ -29,9 +29,9 @@ class Suggestion(Plugin):
|
|||||||
|
|
||||||
if not movies or len(movies) == 0:
|
if not movies or len(movies) == 0:
|
||||||
db = get_session()
|
db = get_session()
|
||||||
active_movies = db.query(Movie) \
|
active_movies = db.query(Media) \
|
||||||
.options(joinedload_all('library')) \
|
.options(joinedload_all('library')) \
|
||||||
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
|
.filter(or_(*[Media.status.has(identifier = s) for s in ['active', 'done']])).all()
|
||||||
movies = [x.library.identifier for x in active_movies]
|
movies = [x.library.identifier for x in active_movies]
|
||||||
|
|
||||||
if not ignored or len(ignored) == 0:
|
if not ignored or len(ignored) == 0:
|
||||||
@@ -89,10 +89,10 @@ class Suggestion(Plugin):
|
|||||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
active_movies = db.query(Movie) \
|
active_movies = db.query(Media) \
|
||||||
.join(Library) \
|
.join(Library) \
|
||||||
.with_entities(Library.identifier) \
|
.with_entities(Library.identifier) \
|
||||||
.filter(Movie.status_id.in_([active_status.get('id'), done_status.get('id')])).all()
|
.filter(Media.status_id.in_([active_status.get('id'), done_status.get('id')])).all()
|
||||||
movies = [x[0] for x in active_movies]
|
movies = [x[0] for x in active_movies]
|
||||||
movies.extend(seen)
|
movies.extend(seen)
|
||||||
|
|
||||||
+24
-24
@@ -5,31 +5,31 @@
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result {
|
.suggestions .media_result {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 33.333%;
|
width: 33.333%;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 960px) {
|
@media all and (max-width: 960px) {
|
||||||
.suggestions .movie_result {
|
.suggestions .media_result {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 600px) {
|
@media all and (max-width: 600px) {
|
||||||
.suggestions .movie_result {
|
.suggestions .media_result {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data {
|
.suggestions .media_result .data {
|
||||||
left: 100px;
|
left: 100px;
|
||||||
background: #4e5969;
|
background: #4e5969;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data .info {
|
.suggestions .media_result .data .info {
|
||||||
top: 15px;
|
top: 15px;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
@@ -37,32 +37,32 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data .info h2 {
|
.suggestions .media_result .data .info h2 {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .rating,
|
.suggestions .media_result .data .info .rating,
|
||||||
.suggestions .movie_result .data .info .genres,
|
.suggestions .media_result .data .info .genres,
|
||||||
.suggestions .movie_result .data .info .year {
|
.suggestions .media_result .data .info .year {
|
||||||
position: static;
|
position: static;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .year {
|
.suggestions .media_result .data .info .year {
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .rating {
|
.suggestions .media_result .data .info .rating {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
float: right;
|
float: right;
|
||||||
margin-top: -20px;
|
margin-top: -20px;
|
||||||
}
|
}
|
||||||
.suggestions .movie_result .data .info .rating:before {
|
.suggestions .media_result .data .info .rating:before {
|
||||||
content: "\e031";
|
content: "\e031";
|
||||||
font-family: 'Elusive-Icons';
|
font-family: 'Elusive-Icons';
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -70,25 +70,25 @@
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .genres {
|
.suggestions .media_result .data .info .genres {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .data {
|
.suggestions .media_result .data {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .options {
|
.suggestions .media_result .options {
|
||||||
left: 100px;
|
left: 100px;
|
||||||
}
|
}
|
||||||
.suggestions .movie_result .options select[name=title] { width: 100%; }
|
.suggestions .media_result .options select[name=title] { width: 100%; }
|
||||||
.suggestions .movie_result .options select[name=profile] { width: 100%; }
|
.suggestions .media_result .options select[name=profile] { width: 100%; }
|
||||||
.suggestions .movie_result .options select[name=category] { width: 100%; }
|
.suggestions .media_result .options select[name=category] { width: 100%; }
|
||||||
|
|
||||||
.suggestions .movie_result .button {
|
.suggestions .media_result .button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin: 2px 0 0 0;
|
margin: 2px 0 0 0;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
@@ -96,25 +96,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.suggestions .movie_result .thumbnail {
|
.suggestions .media_result .thumbnail {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .actions {
|
.suggestions .media_result .actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
display: none;
|
display: none;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
}
|
}
|
||||||
.suggestions .movie_result:hover .actions {
|
.suggestions .media_result:hover .actions {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.suggestions .movie_result .data.open .actions {
|
.suggestions .media_result .data.open .actions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions .movie_result .actions a {
|
.suggestions .media_result .actions a {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
+3
-3
@@ -17,7 +17,7 @@ var SuggestList = new Class({
|
|||||||
'click:relay(a.delete)': function(e, el){
|
'click:relay(a.delete)': function(e, el){
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
$(el).getParent('.movie_result').destroy();
|
$(el).getParent('.media_result').destroy();
|
||||||
|
|
||||||
Api.request('suggestion.ignore', {
|
Api.request('suggestion.ignore', {
|
||||||
'data': {
|
'data': {
|
||||||
@@ -30,7 +30,7 @@ var SuggestList = new Class({
|
|||||||
'click:relay(a.eye-open)': function(e, el){
|
'click:relay(a.eye-open)': function(e, el){
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
$(el).getParent('.movie_result').destroy();
|
$(el).getParent('.media_result').destroy();
|
||||||
|
|
||||||
Api.request('suggestion.ignore', {
|
Api.request('suggestion.ignore', {
|
||||||
'data': {
|
'data': {
|
||||||
@@ -65,7 +65,7 @@ var SuggestList = new Class({
|
|||||||
|
|
||||||
Object.each(json.suggestions, function(movie){
|
Object.each(json.suggestions, function(movie){
|
||||||
|
|
||||||
var m = new Block.Search.Item(movie, {
|
var m = new Block.Search.MovieItem(movie, {
|
||||||
'onAdded': function(){
|
'onAdded': function(){
|
||||||
self.afterAdded(m, movie)
|
self.afterAdded(m, movie)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import json
|
||||||
|
from couchpotato import CPLog
|
||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
|
import requests
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PlexClientProtocol(object):
|
||||||
|
def __init__(self, plex):
|
||||||
|
self.plex = plex
|
||||||
|
|
||||||
|
addEvent('notify.plex.notifyClient', self.notify)
|
||||||
|
|
||||||
|
def notify(self, client, message):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class PlexClientHTTP(PlexClientProtocol):
|
||||||
|
def request(self, command, client):
|
||||||
|
url = 'http://%s:%s/xbmcCmds/xbmcHttp/?%s' % (
|
||||||
|
client['address'],
|
||||||
|
client['port'],
|
||||||
|
tryUrlencode(command)
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False)
|
||||||
|
except Exception, err:
|
||||||
|
log.error("Couldn't sent command to Plex: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def notify(self, client, message):
|
||||||
|
if client.get('protocol') != 'xbmchttp':
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'command': 'ExecBuiltIn',
|
||||||
|
'parameter': 'Notification(CouchPotato, %s)' % message
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.request(data, client)
|
||||||
|
|
||||||
|
|
||||||
|
class PlexClientJSON(PlexClientProtocol):
|
||||||
|
def request(self, method, params, client):
|
||||||
|
log.debug('sendJSON("%s", %s, %s)', (method, params, client))
|
||||||
|
url = 'http://%s:%s/jsonrpc' % (
|
||||||
|
client['address'],
|
||||||
|
client['port']
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
request = {
|
||||||
|
'id': 1,
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': method,
|
||||||
|
'params': params
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
|
||||||
|
except Exception, err:
|
||||||
|
log.error("Couldn't sent command to Plex: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def notify(self, client, message):
|
||||||
|
if client.get('protocol') not in ['xbmcjson', 'plex']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'title': 'CouchPotato',
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
return self.request('GUI.ShowNotification', params, client)
|
||||||
@@ -1,183 +1,64 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost
|
|
||||||
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 datetime import datetime
|
from .client import PlexClientHTTP, PlexClientJSON
|
||||||
from urlparse import urlparse
|
from .server import PlexServer
|
||||||
from xml.dom import minidom
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
try:
|
|
||||||
import xml.etree.cElementTree as etree
|
|
||||||
except ImportError:
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Plex(Notification):
|
class Plex(Notification):
|
||||||
|
|
||||||
client_update_time = 5 * 60
|
|
||||||
http_time_between_calls = 0
|
http_time_between_calls = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(Plex, self).__init__()
|
super(Plex, self).__init__()
|
||||||
|
|
||||||
self.clients = {}
|
self.server = PlexServer(self)
|
||||||
self.clients_updated = None
|
|
||||||
|
self.client_protocols = {
|
||||||
|
'http': PlexClientHTTP(self),
|
||||||
|
'json': PlexClientJSON(self)
|
||||||
|
}
|
||||||
|
|
||||||
addEvent('renamer.after', self.addToLibrary)
|
addEvent('renamer.after', self.addToLibrary)
|
||||||
|
|
||||||
def updateClients(self, force = False):
|
|
||||||
if not self.conf('media_server'):
|
|
||||||
log.warning("Plex media server hostname is required")
|
|
||||||
return
|
|
||||||
|
|
||||||
since_update = ((datetime.now() - self.clients_updated).total_seconds())\
|
|
||||||
if self.clients_updated is not None else None
|
|
||||||
|
|
||||||
if force or self.clients_updated is None or since_update > self.client_update_time:
|
|
||||||
self.clients = {}
|
|
||||||
|
|
||||||
data = self.urlopen('%s/clients' % self.createHost(self.conf('media_server'), port = 32400))
|
|
||||||
client_result = etree.fromstring(data)
|
|
||||||
|
|
||||||
clients = [x.strip().lower() for x in self.conf('clients').split(',')]
|
|
||||||
|
|
||||||
for server in client_result.findall('Server'):
|
|
||||||
if server.get('name').lower() in clients:
|
|
||||||
clients.remove(server.get('name').lower())
|
|
||||||
protocol = server.get('protocol', 'xbmchttp')
|
|
||||||
|
|
||||||
if protocol in ['plex', 'xbmcjson', 'xbmchttp']:
|
|
||||||
self.clients[server.get('name')] = {
|
|
||||||
'name': server.get('name'),
|
|
||||||
'address': server.get('address'),
|
|
||||||
'port': server.get('port'),
|
|
||||||
'protocol': protocol
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(clients) > 0:
|
|
||||||
log.info2('Unable to find plex clients: %s', ', '.join(clients))
|
|
||||||
|
|
||||||
log.info2('Found hosts: %s', ', '.join(self.clients.keys()))
|
|
||||||
|
|
||||||
self.clients_updated = datetime.now()
|
|
||||||
|
|
||||||
|
|
||||||
def addToLibrary(self, message = None, group = {}):
|
def addToLibrary(self, message = None, group = {}):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
log.info('Sending notification to Plex')
|
return self.server.refresh()
|
||||||
|
|
||||||
source_type = ['movie']
|
def getClientNames(self):
|
||||||
base_url = '%s/library/sections' % self.createHost(self.conf('media_server'), port = 32400)
|
return [
|
||||||
refresh_url = '%s/%%s/refresh' % base_url
|
x.strip().lower()
|
||||||
|
for x in self.conf('clients').split(',')
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
def notifyClients(self, message, client_names):
|
||||||
sections_xml = self.urlopen(base_url)
|
success = True
|
||||||
xml_sections = minidom.parseString(sections_xml)
|
|
||||||
sections = xml_sections.getElementsByTagName('Directory')
|
|
||||||
|
|
||||||
for s in sections:
|
for client_name in client_names:
|
||||||
if s.getAttribute('type') in source_type:
|
|
||||||
url = refresh_url % s.getAttribute('key')
|
|
||||||
x = self.urlopen(url)
|
|
||||||
|
|
||||||
except:
|
client_success = False
|
||||||
log.error('Plex library update failed for %s, Media Server not running: %s',
|
client = self.server.clients.get(client_name)
|
||||||
(self.conf('media_server'), traceback.format_exc(1)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
if client and client['found']:
|
||||||
|
client_success = fireEvent('notify.plex.notifyClient', client, message, single = True)
|
||||||
|
|
||||||
def sendHTTP(self, command, client):
|
if not client_success:
|
||||||
url = 'http://%s:%s/xbmcCmds/xbmcHttp/?%s' % (
|
if self.server.staleClients() or not client:
|
||||||
client['address'],
|
log.info('Failed to send notification to client "%s". '
|
||||||
client['port'],
|
'Client list is stale, updating the client list and retrying.', client_name)
|
||||||
tryUrlencode(command)
|
self.server.updateClients(self.getClientNames())
|
||||||
)
|
else:
|
||||||
|
log.warning('Failed to send notification to client %s, skipping this time', client_name)
|
||||||
|
success = False
|
||||||
|
|
||||||
headers = {}
|
return success
|
||||||
|
|
||||||
try:
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
self.urlopen(url, headers = headers, timeout = 3, show_error = False)
|
return self.notifyClients(message, self.getClientNames())
|
||||||
except Exception, err:
|
|
||||||
log.error("Couldn't sent command to Plex: %s", err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def notifyHTTP(self, message = '', data = {}, listener = None):
|
|
||||||
total = 0
|
|
||||||
successful = 0
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'command': 'ExecBuiltIn',
|
|
||||||
'parameter': 'Notification(CouchPotato, %s)' % message
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, client in self.clients.items():
|
|
||||||
if client['protocol'] == 'xbmchttp':
|
|
||||||
total += 1
|
|
||||||
if self.sendHTTP(data, client):
|
|
||||||
successful += 1
|
|
||||||
|
|
||||||
return successful == total
|
|
||||||
|
|
||||||
def sendJSON(self, method, params, client):
|
|
||||||
log.debug('sendJSON("%s", %s, %s)', (method, params, client))
|
|
||||||
url = 'http://%s:%s/jsonrpc' % (
|
|
||||||
client['address'],
|
|
||||||
client['port']
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
request = {
|
|
||||||
'id':1,
|
|
||||||
'jsonrpc': '2.0',
|
|
||||||
'method': method,
|
|
||||||
'params': params
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
|
|
||||||
except Exception, err:
|
|
||||||
log.error("Couldn't sent command to Plex: %s", err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def notifyJSON(self, message = '', data = {}, listener = None):
|
|
||||||
total = 0
|
|
||||||
successful = 0
|
|
||||||
|
|
||||||
params = {
|
|
||||||
'title': 'CouchPotato',
|
|
||||||
'message': message
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, client in self.clients.items():
|
|
||||||
if client['protocol'] in ['xbmcjson', 'plex']:
|
|
||||||
total += 1
|
|
||||||
if self.sendJSON('GUI.ShowNotification', params, client):
|
|
||||||
successful += 1
|
|
||||||
|
|
||||||
return successful == total
|
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None, force = False):
|
|
||||||
self.updateClients(force)
|
|
||||||
|
|
||||||
http_result = self.notifyHTTP(message, data, listener)
|
|
||||||
json_result = self.notifyJSON(message, data, listener)
|
|
||||||
|
|
||||||
return http_result and json_result
|
|
||||||
|
|
||||||
def test(self, **kwargs):
|
def test(self, **kwargs):
|
||||||
|
|
||||||
@@ -185,24 +66,12 @@ class Plex(Notification):
|
|||||||
|
|
||||||
log.info('Sending test to %s', test_type)
|
log.info('Sending test to %s', test_type)
|
||||||
|
|
||||||
success = self.notify(
|
notify_success = self.notify(
|
||||||
message = self.test_message,
|
message = self.test_message,
|
||||||
data = {},
|
data = {},
|
||||||
listener = 'test',
|
listener = 'test'
|
||||||
force = True
|
|
||||||
)
|
)
|
||||||
success2 = self.addToLibrary()
|
|
||||||
|
|
||||||
return {
|
refresh_success = self.addToLibrary()
|
||||||
'success': success or success2
|
|
||||||
}
|
|
||||||
|
|
||||||
def createHost(self, host, port = None):
|
return {'success': notify_success or refresh_success}
|
||||||
|
|
||||||
h = cleanHost(host)
|
|
||||||
p = urlparse(h)
|
|
||||||
h = h.rstrip('/')
|
|
||||||
if port and not p.port:
|
|
||||||
h += ':%s' % port
|
|
||||||
|
|
||||||
return h
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
from datetime import timedelta, datetime
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
|
from couchpotato import CPLog
|
||||||
|
from urlparse import urlparse
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import xml.etree.cElementTree as etree
|
||||||
|
except ImportError:
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PlexServer(object):
|
||||||
|
def __init__(self, plex):
|
||||||
|
self.plex = plex
|
||||||
|
|
||||||
|
self.clients = {}
|
||||||
|
self.last_clients_update = None
|
||||||
|
|
||||||
|
def staleClients(self):
|
||||||
|
if not self.last_clients_update:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return self.last_clients_update + timedelta(minutes=15) < datetime.now()
|
||||||
|
|
||||||
|
def request(self, path, data_type='xml'):
|
||||||
|
if not self.plex.conf('media_server'):
|
||||||
|
log.warning("Plex media server hostname is required")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if path.startswith('/'):
|
||||||
|
path = path[1:]
|
||||||
|
|
||||||
|
data = self.plex.urlopen('%s/%s' % (
|
||||||
|
self.createHost(self.plex.conf('media_server'), port = 32400),
|
||||||
|
path
|
||||||
|
))
|
||||||
|
|
||||||
|
if data_type == 'xml':
|
||||||
|
return etree.fromstring(data)
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
|
def updateClients(self, client_names):
|
||||||
|
log.info('Searching for clients on Plex Media Server')
|
||||||
|
|
||||||
|
self.clients = {}
|
||||||
|
|
||||||
|
result = self.request('clients')
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
found_clients = [
|
||||||
|
c for c in result.findall('Server')
|
||||||
|
if c.get('name') and c.get('name').lower() in client_names
|
||||||
|
]
|
||||||
|
|
||||||
|
# Store client details in cache
|
||||||
|
for client in found_clients:
|
||||||
|
name = client.get('name').lower()
|
||||||
|
|
||||||
|
self.clients[name] = {
|
||||||
|
'name': client.get('name'),
|
||||||
|
'found': True,
|
||||||
|
'address': client.get('address'),
|
||||||
|
'port': client.get('port'),
|
||||||
|
'protocol': client.get('protocol', 'xbmchttp')
|
||||||
|
}
|
||||||
|
|
||||||
|
client_names.remove(name)
|
||||||
|
|
||||||
|
# Store dummy info for missing clients
|
||||||
|
for client_name in client_names:
|
||||||
|
self.clients[client_name] = {
|
||||||
|
'found': False
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(client_names) > 0:
|
||||||
|
log.debug('Unable to find clients: %s', ', '.join(client_names))
|
||||||
|
|
||||||
|
self.last_clients_update = datetime.now()
|
||||||
|
|
||||||
|
def refresh(self, section_types=None):
|
||||||
|
if not section_types:
|
||||||
|
section_types = ['movie']
|
||||||
|
|
||||||
|
sections = self.request('library/sections')
|
||||||
|
|
||||||
|
try:
|
||||||
|
for section in sections.findall('Directory'):
|
||||||
|
if section.get('type') not in section_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.request('library/sections/%s/refresh' % section.get('key'), 'text')
|
||||||
|
except:
|
||||||
|
log.error('Plex library update failed for %s, Media Server not running: %s',
|
||||||
|
(self.plex.conf('media_server'), traceback.format_exc(1)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def createHost(self, host, port = None):
|
||||||
|
|
||||||
|
h = cleanHost(host)
|
||||||
|
p = urlparse(h)
|
||||||
|
h = h.rstrip('/')
|
||||||
|
|
||||||
|
if port and not p.port:
|
||||||
|
h += ':%s' % port
|
||||||
|
|
||||||
|
return h
|
||||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
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, Category
|
from couchpotato.core.settings.model import Media, Category
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class CategoryPlugin(Plugin):
|
|||||||
def removeFromMovie(self, category_id):
|
def removeFromMovie(self, category_id):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
movies = db.query(Movie).filter(Movie.category_id == category_id).all()
|
movies = db.query(Media).filter(Media.category_id == category_id).all()
|
||||||
|
|
||||||
if len(movies) > 0:
|
if len(movies) > 0:
|
||||||
for movie in movies:
|
for movie in movies:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import fireEvent
|
|||||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||||
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 Media, Library, LibraryTitle, \
|
||||||
Release
|
Release
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import asc, or_
|
from sqlalchemy.sql.expression import asc, or_
|
||||||
@@ -50,12 +50,12 @@ class Dashboard(Plugin):
|
|||||||
|
|
||||||
# Get all active movies
|
# Get all active movies
|
||||||
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
|
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
|
||||||
q = db.query(Movie) \
|
q = db.query(Media) \
|
||||||
.join(Library) \
|
.join(Library) \
|
||||||
.outerjoin(Movie.releases) \
|
.outerjoin(Media.releases) \
|
||||||
.filter(Movie.status_id == active_status.get('id')) \
|
.filter(Media.status_id == active_status.get('id')) \
|
||||||
.with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
|
.with_entities(Media.id, Media.profile_id, Library.info, Library.year) \
|
||||||
.group_by(Movie.id) \
|
.group_by(Media.id) \
|
||||||
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
|
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
|
||||||
|
|
||||||
if not random:
|
if not random:
|
||||||
@@ -101,11 +101,11 @@ class Dashboard(Plugin):
|
|||||||
if len(movie_ids) > 0:
|
if len(movie_ids) > 0:
|
||||||
|
|
||||||
# Get all movie information
|
# Get all movie information
|
||||||
movies_raw = db.query(Movie) \
|
movies_raw = db.query(Media) \
|
||||||
.options(joinedload_all('library.titles')) \
|
.options(joinedload_all('library.titles')) \
|
||||||
.options(joinedload_all('library.files')) \
|
.options(joinedload_all('library.files')) \
|
||||||
.options(joinedload_all('files')) \
|
.options(joinedload_all('files')) \
|
||||||
.filter(Movie.id.in_(movie_ids)) \
|
.filter(Media.id.in_(movie_ids)) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
# Create dict by movie id
|
# Create dict by movie id
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent, fireEvent
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
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 Profile, ProfileType, Movie
|
from couchpotato.core.settings.model import Profile, ProfileType, Media
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -38,7 +38,7 @@ class ProfilePlugin(Plugin):
|
|||||||
active_status = fireEvent('status.get', 'active', single = True)
|
active_status = fireEvent('status.get', 'active', single = True)
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
|
movies = db.query(Media).filter(Media.status_id == active_status.get('id'), Media.profile == None).all()
|
||||||
|
|
||||||
if len(movies) > 0:
|
if len(movies) > 0:
|
||||||
default_profile = self.default()
|
default_profile = self.default()
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ from couchpotato.core.helpers.encoding import ss
|
|||||||
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.plugins.scanner.main import Scanner
|
from couchpotato.core.plugins.scanner.main import Scanner
|
||||||
from couchpotato.core.settings.model import File, Release as Relea, Movie
|
from couchpotato.core.settings.model import File, Release as Relea, Media
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
import os
|
import os
|
||||||
import traceback
|
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -50,6 +50,38 @@ class Release(Plugin):
|
|||||||
addEvent('release.clean', self.clean)
|
addEvent('release.clean', self.clean)
|
||||||
addEvent('release.update_status', self.updateStatus)
|
addEvent('release.update_status', self.updateStatus)
|
||||||
|
|
||||||
|
# Clean releases that didn't have activity in the last week
|
||||||
|
addEvent('app.load', self.cleanReleases)
|
||||||
|
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
|
||||||
|
|
||||||
|
def cleanReleases(self):
|
||||||
|
|
||||||
|
log.debug('Removing releases from dashboard')
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
week = 262080
|
||||||
|
|
||||||
|
done_status, available_status, snatched_status, downloaded_status, ignored_status = \
|
||||||
|
fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded', 'ignored'], single = True)
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
# get movies last_edit more than a week ago
|
||||||
|
media = db.query(Media) \
|
||||||
|
.filter(Media.status_id == done_status.get('id'), Media.last_edit < (now - week)) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
for item in media:
|
||||||
|
for rel in item.releases:
|
||||||
|
# Remove all available releases
|
||||||
|
if rel.status_id in [available_status.get('id')]:
|
||||||
|
fireEvent('release.delete', id = rel.id, single = True)
|
||||||
|
# Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move
|
||||||
|
elif rel.status_id in [snatched_status.get('id'), downloaded_status.get('id')]:
|
||||||
|
fireEvent('release.update', id = rel.id, status = ignored_status)
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
def add(self, group):
|
def add(self, group):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
@@ -60,9 +92,9 @@ class Release(Plugin):
|
|||||||
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
|
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
|
||||||
|
|
||||||
# Add movie
|
# Add movie
|
||||||
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
|
movie = db.query(Media).filter_by(library_id = group['library'].get('id')).first()
|
||||||
if not movie:
|
if not movie:
|
||||||
movie = Movie(
|
movie = Media(
|
||||||
library_id = group['library'].get('id'),
|
library_id = group['library'].get('id'),
|
||||||
profile_id = 0,
|
profile_id = 0,
|
||||||
status_id = done_status.get('id')
|
status_id = done_status.get('id')
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ rename_options = {
|
|||||||
'cd': 'CD number (cd1)',
|
'cd': 'CD number (cd1)',
|
||||||
'cd_nr': 'Just the cd nr. (1)',
|
'cd_nr': 'Just the cd nr. (1)',
|
||||||
'mpaa': 'MPAA Rating',
|
'mpaa': 'MPAA Rating',
|
||||||
|
'category': 'Category label',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class Renamer(Plugin):
|
|||||||
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info))
|
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info))
|
||||||
|
|
||||||
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
|
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
|
||||||
files = files, download_info = download_info, return_ignored = False, single = True)
|
files = files, download_info = download_info, return_ignored = False, single = True) or []
|
||||||
|
|
||||||
folder_name = self.conf('folder_name')
|
folder_name = self.conf('folder_name')
|
||||||
file_name = self.conf('file_name')
|
file_name = self.conf('file_name')
|
||||||
@@ -173,7 +173,12 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
# Overwrite destination when set in category
|
# Overwrite destination when set in category
|
||||||
destination = self.conf('to')
|
destination = self.conf('to')
|
||||||
|
category_label = ''
|
||||||
for movie in library_ent.movies:
|
for movie in library_ent.movies:
|
||||||
|
|
||||||
|
if movie.category and movie.category.label:
|
||||||
|
category_label = movie.category.label
|
||||||
|
|
||||||
if movie.category and movie.category.destination and len(movie.category.destination) > 0 and movie.category.destination != 'None':
|
if movie.category and movie.category.destination and len(movie.category.destination) > 0 and movie.category.destination != 'None':
|
||||||
destination = movie.category.destination
|
destination = movie.category.destination
|
||||||
log.debug('Setting category destination for "%s": %s' % (movie_title, destination))
|
log.debug('Setting category destination for "%s": %s' % (movie_title, destination))
|
||||||
@@ -217,6 +222,7 @@ class Renamer(Plugin):
|
|||||||
'cd': '',
|
'cd': '',
|
||||||
'cd_nr': '',
|
'cd_nr': '',
|
||||||
'mpaa': library['info'].get('mpaa', ''),
|
'mpaa': library['info'].get('mpaa', ''),
|
||||||
|
'category': category_label,
|
||||||
}
|
}
|
||||||
|
|
||||||
for file_type in group['files']:
|
for file_type in group['files']:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \
|
|||||||
splitString
|
splitString
|
||||||
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, Movie
|
from couchpotato.core.settings.model import File, Media
|
||||||
from enzyme.exceptions import NoParserError, ParseError
|
from enzyme.exceptions import NoParserError, ParseError
|
||||||
from guessit import guess_movie_info
|
from guessit import guess_movie_info
|
||||||
from subliminal.videos import Video
|
from subliminal.videos import Video
|
||||||
@@ -415,7 +415,7 @@ class Scanner(Plugin):
|
|||||||
if not group['library']:
|
if not group['library']:
|
||||||
log.error('Unable to determine movie: %s', group['identifiers'])
|
log.error('Unable to determine movie: %s', group['identifiers'])
|
||||||
else:
|
else:
|
||||||
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
|
movie = db.query(Media).filter_by(library_id = group['library']['id']).first()
|
||||||
group['movie_id'] = None if not movie else movie.id
|
group['movie_id'] = None if not movie else movie.id
|
||||||
|
|
||||||
processed_movies[identifier] = group
|
processed_movies[identifier] = group
|
||||||
@@ -830,19 +830,21 @@ class Scanner(Plugin):
|
|||||||
def findYear(self, text):
|
def findYear(self, text):
|
||||||
|
|
||||||
# Search year inside () or [] first
|
# Search year inside () or [] first
|
||||||
matches = re.search('(\(|\[)(?P<year>19[0-9]{2}|20[0-9]{2})(\]|\))', text)
|
matches = re.findall('(\(|\[)(?P<year>19[0-9]{2}|20[0-9]{2})(\]|\))', text)
|
||||||
if matches:
|
if matches:
|
||||||
return matches.group('year')
|
return matches[-1][1]
|
||||||
|
|
||||||
# Search normal
|
# Search normal
|
||||||
matches = re.search('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
|
matches = re.findall('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
|
||||||
if matches:
|
if matches:
|
||||||
return matches.group('year')
|
return matches[-1]
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def getReleaseNameYear(self, release_name, file_name = None):
|
def getReleaseNameYear(self, release_name, file_name = None):
|
||||||
|
|
||||||
|
release_name = release_name.strip(' .-_')
|
||||||
|
|
||||||
# Use guessit first
|
# Use guessit first
|
||||||
guess = {}
|
guess = {}
|
||||||
if file_name:
|
if file_name:
|
||||||
@@ -860,7 +862,7 @@ class Scanner(Plugin):
|
|||||||
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)
|
||||||
|
|
||||||
for year_str in [file_name, cleaned]:
|
for year_str in [file_name, release_name, cleaned]:
|
||||||
if not year_str: continue
|
if not year_str: continue
|
||||||
year = self.findYear(year_str)
|
year = self.findYear(year_str)
|
||||||
if year:
|
if year:
|
||||||
@@ -870,19 +872,21 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
if year: # Split name on year
|
if year: # Split name on year
|
||||||
try:
|
try:
|
||||||
movie_name = cleaned.split(year).pop(0).strip()
|
movie_name = cleaned.rsplit(year, 1).pop(0).strip()
|
||||||
cp_guess = {
|
if movie_name:
|
||||||
'name': movie_name,
|
cp_guess = {
|
||||||
'year': int(year),
|
'name': movie_name,
|
||||||
}
|
'year': int(year),
|
||||||
|
}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
else: # Split name on multiple spaces
|
|
||||||
|
if not cp_guess: # Split name on multiple spaces
|
||||||
try:
|
try:
|
||||||
movie_name = cleaned.split(' ').pop(0).strip()
|
movie_name = cleaned.split(' ').pop(0).strip()
|
||||||
cp_guess = {
|
cp_guess = {
|
||||||
'name': movie_name,
|
'name': movie_name,
|
||||||
'year': int(year),
|
'year': int(year) if movie_name[:4] != year else 0,
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -14,25 +14,25 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result {
|
.page.userscript .media_result {
|
||||||
height: 140px;
|
height: 140px;
|
||||||
}
|
}
|
||||||
.page.userscript .movie_result .thumbnail {
|
.page.userscript .media_result .thumbnail {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
}
|
}
|
||||||
.page.userscript .movie_result .options {
|
.page.userscript .media_result .options {
|
||||||
left: 90px;
|
left: 90px;
|
||||||
padding: 54px 15px;
|
padding: 54px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result .year {
|
.page.userscript .media_result .year {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result .options select[name="title"] {
|
.page.userscript .media_result .options select[name="title"] {
|
||||||
width: 190px;
|
width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result .options select[name="profile"] {
|
.page.userscript .media_result .options select[name="profile"] {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Page.Userscript = new Class({
|
|||||||
if(json.error)
|
if(json.error)
|
||||||
self.frame.set('html', json.error);
|
self.frame.set('html', json.error);
|
||||||
else {
|
else {
|
||||||
var item = new Block.Search.Item(json.movie);
|
var item = new Block.Search.MovieItem(json.movie);
|
||||||
self.frame.adopt(item);
|
self.frame.adopt(item);
|
||||||
item.showOptions();
|
item.showOptions();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.automation.base import Automation
|
from couchpotato.core.providers.automation.base import Automation
|
||||||
import json
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ class Flixster(Automation):
|
|||||||
if not enablers[index]:
|
if not enablers[index]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data = json.loads(self.getHTMLData(self.url % user_id))
|
data = self.getJsonData(self.url % user_id, decode_from = 'iso-8859-1')
|
||||||
|
|
||||||
for movie in data:
|
for movie in data:
|
||||||
movies.append({'title': movie['movie']['title'], 'year': movie['movie']['year'] })
|
movies.append({'title': movie['movie']['title'], 'year': movie['movie']['year'] })
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ class ITunes(Automation, RSS):
|
|||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
|
|
||||||
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
|
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
|
||||||
possibleTitles, getTitle
|
possibleTitles, getTitle
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
@@ -7,6 +8,7 @@ from couchpotato.environment import Env
|
|||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
import cookielib
|
import cookielib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -15,7 +17,6 @@ import xml.etree.ElementTree as XMLTree
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MultiProvider(Plugin):
|
class MultiProvider(Plugin):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -63,13 +64,17 @@ class Provider(Plugin):
|
|||||||
|
|
||||||
return self.is_available.get(host, False)
|
return self.is_available.get(host, False)
|
||||||
|
|
||||||
def getJsonData(self, url, **kwargs):
|
def getJsonData(self, url, decode_from = None, **kwargs):
|
||||||
|
|
||||||
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
|
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
|
||||||
data = self.getCache(cache_key, url, **kwargs)
|
data = self.getCache(cache_key, url, **kwargs)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
try:
|
try:
|
||||||
|
data = data.strip()
|
||||||
|
if decode_from:
|
||||||
|
data = data.decode(decode_from)
|
||||||
|
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
except:
|
except:
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
@@ -279,8 +284,7 @@ class ResultList(list):
|
|||||||
|
|
||||||
new_result = self.fillResult(result)
|
new_result = self.fillResult(result)
|
||||||
|
|
||||||
is_correct_movie = fireEvent('movie.searcher.correct_movie',
|
is_correct_movie = fireEvent('searcher.correct_release', new_result, self.movie, self.quality,
|
||||||
nzb = new_result, movie = self.movie, quality = self.quality,
|
|
||||||
imdb_results = self.kwargs.get('imdb_results', False), single = True)
|
imdb_results = self.kwargs.get('imdb_results', False), single = True)
|
||||||
|
|
||||||
if is_correct_movie and new_result['id'] not in self.result_ids:
|
if is_correct_movie and new_result['id'] not in self.result_ids:
|
||||||
|
|||||||
@@ -32,9 +32,32 @@ class MovieResultModifier(Plugin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
addEvent('result.modify.info.search', self.returnByType)
|
||||||
addEvent('result.modify.movie.search', self.combineOnIMDB)
|
addEvent('result.modify.movie.search', self.combineOnIMDB)
|
||||||
addEvent('result.modify.movie.info', self.checkLibrary)
|
addEvent('result.modify.movie.info', self.checkLibrary)
|
||||||
|
|
||||||
|
def returnByType(self, results):
|
||||||
|
|
||||||
|
new_results = {'unknown':[]}
|
||||||
|
for r in results:
|
||||||
|
if r.get('type'):
|
||||||
|
type_name = r.get('type') + 's'
|
||||||
|
if not new_results.has_key(type_name):
|
||||||
|
new_results[type_name] = []
|
||||||
|
|
||||||
|
new_results[type_name].append(r)
|
||||||
|
else:
|
||||||
|
new_results['unknown'].append(r)
|
||||||
|
|
||||||
|
if len(new_results['unknown']) == 0:
|
||||||
|
del new_results['unknown']
|
||||||
|
|
||||||
|
# Combine movies, needs a cleaner way..
|
||||||
|
if new_results.has_key('movies'):
|
||||||
|
new_results['movies'] = self.combineOnIMDB(new_results['movies'])
|
||||||
|
|
||||||
|
return new_results
|
||||||
|
|
||||||
def combineOnIMDB(self, results):
|
def combineOnIMDB(self, results):
|
||||||
|
|
||||||
temp = {}
|
temp = {}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class OMDBAPI(MovieProvider):
|
|||||||
year = tryInt(movie.get('Year', ''))
|
year = tryInt(movie.get('Year', ''))
|
||||||
|
|
||||||
movie_data = {
|
movie_data = {
|
||||||
|
'type': 'movie',
|
||||||
'via_imdb': True,
|
'via_imdb': True,
|
||||||
'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'),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ log = CPLog(__name__)
|
|||||||
class TheMovieDb(MovieProvider):
|
class TheMovieDb(MovieProvider):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
addEvent('info.search', self.search, priority = 2)
|
||||||
addEvent('movie.search', self.search, priority = 2)
|
addEvent('movie.search', self.search, priority = 2)
|
||||||
addEvent('movie.info', self.getInfo, priority = 2)
|
addEvent('movie.info', self.getInfo, priority = 2)
|
||||||
addEvent('movie.info_by_tmdb', self.getInfo)
|
addEvent('movie.info_by_tmdb', self.getInfo)
|
||||||
@@ -103,6 +104,7 @@ class TheMovieDb(MovieProvider):
|
|||||||
year = None
|
year = None
|
||||||
|
|
||||||
movie_data = {
|
movie_data = {
|
||||||
|
'type': 'movie',
|
||||||
'via_tmdb': True,
|
'via_tmdb': True,
|
||||||
'tmdb_id': movie.id,
|
'tmdb_id': movie.id,
|
||||||
'titles': [toUnicode(movie.title)],
|
'titles': [toUnicode(movie.title)],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||||
from couchpotato.core.helpers.rss import RSS
|
from couchpotato.core.helpers.rss import RSS
|
||||||
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
|
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
@@ -83,7 +83,7 @@ class Newznab(NZBProvider, RSS):
|
|||||||
results.append({
|
results.append({
|
||||||
'id': nzb_id,
|
'id': nzb_id,
|
||||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||||
'name': name,
|
'name': toUnicode(name),
|
||||||
'name_extra': name_extra,
|
'name_extra': name_extra,
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ class Yify(TorrentProvider):
|
|||||||
|
|
||||||
return super(Yify, self).search(movie, quality)
|
return super(Yify, self).search(movie, quality)
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
data = self.getJsonData(self.urls['search'] % (title, quality['identifier']))
|
data = self.getJsonData(self.urls['search'] % (movie['library']['identifier'], quality['identifier']))
|
||||||
|
|
||||||
if data and data.get('MovieList'):
|
if data and data.get('MovieList'):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class Movie(Entity):
|
|||||||
such as trailers, nfo, thumbnails"""
|
such as trailers, nfo, thumbnails"""
|
||||||
|
|
||||||
last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
|
last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
|
||||||
|
type = 'movie' # Compat tv branch
|
||||||
|
|
||||||
library = ManyToOne('Library', cascade = 'delete, delete-orphan', single_parent = True)
|
library = ManyToOne('Library', cascade = 'delete, delete-orphan', single_parent = True)
|
||||||
status = ManyToOne('Status')
|
status = ManyToOne('Status')
|
||||||
@@ -86,6 +87,7 @@ class Movie(Entity):
|
|||||||
releases = OneToMany('Release', cascade = 'all, delete-orphan')
|
releases = OneToMany('Release', cascade = 'all, delete-orphan')
|
||||||
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
|
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
|
||||||
|
|
||||||
|
Media = Movie # Compat tv branch
|
||||||
|
|
||||||
class Library(Entity):
|
class Library(Entity):
|
||||||
""""""
|
""""""
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self));
|
self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self));
|
||||||
|
|
||||||
// Check if device is touchenabled
|
// Check if device is touchenabled
|
||||||
self.touch_device = 'ontouchstart' in document.documentElement;
|
self.touch_device = 'ontouchstart' in window || navigator.msMaxTouchPoints;
|
||||||
if(self.touch_device)
|
if(self.touch_device)
|
||||||
self.c.addClass('touch_enabled');
|
self.c.addClass('touch_enabled');
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ var AboutSettingTab = new Class({
|
|||||||
new Element('div.donate', {
|
new Element('div.donate', {
|
||||||
'html':
|
'html':
|
||||||
'Or support me via:' +
|
'Or support me via:' +
|
||||||
'<iframe src="http://couchpota.to/donate.html" style="border:none; height: 200px;" scrolling="no"></iframe>'
|
'<iframe src="https://couchpota.to/donate.html" style="border:none; height: 200px;" scrolling="no"></iframe>'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Page.Home = new Class({
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
'filter': {
|
'filter': {
|
||||||
'release_status': 'snatched,available'
|
'release_status': 'snatched,seeding,missing,available,downloaded'
|
||||||
},
|
},
|
||||||
'limit': null,
|
'limit': null,
|
||||||
'onLoaded': function(){
|
'onLoaded': function(){
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ else
|
|||||||
echo "/etc/default/couchpotato not found using default settings.";
|
echo "/etc/default/couchpotato not found using default settings.";
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
. /lib/lsb/init-functions
|
||||||
|
|
||||||
# Script name
|
# Script name
|
||||||
NAME=couchpotato
|
NAME=couchpotato
|
||||||
|
|
||||||
|
|||||||
+38
-15
@@ -17,18 +17,21 @@
|
|||||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
import urllib
|
||||||
|
import os.path
|
||||||
|
import time
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
from rtorrent.common import find_torrent, \
|
from rtorrent.common import find_torrent, \
|
||||||
is_valid_port, convert_version_tuple_to_str
|
is_valid_port, convert_version_tuple_to_str
|
||||||
from rtorrent.lib.torrentparser import TorrentParser
|
from rtorrent.lib.torrentparser import TorrentParser
|
||||||
from rtorrent.lib.xmlrpc.http import HTTPServerProxy
|
from rtorrent.lib.xmlrpc.http import HTTPServerProxy
|
||||||
from rtorrent.rpc import Method, BasicAuthTransport
|
from rtorrent.lib.xmlrpc.scgi import SCGIServerProxy
|
||||||
|
from rtorrent.rpc import Method
|
||||||
|
from rtorrent.lib.xmlrpc.basic_auth import BasicAuthTransport
|
||||||
from rtorrent.torrent import Torrent
|
from rtorrent.torrent import Torrent
|
||||||
from rtorrent.group import Group
|
from rtorrent.group import Group
|
||||||
import os.path
|
|
||||||
import rtorrent.rpc # @UnresolvedImport
|
import rtorrent.rpc # @UnresolvedImport
|
||||||
import time
|
|
||||||
import xmlrpclib
|
|
||||||
|
|
||||||
__version__ = "0.2.9"
|
__version__ = "0.2.9"
|
||||||
__author__ = "Chris Lucas"
|
__author__ = "Chris Lucas"
|
||||||
@@ -43,13 +46,25 @@ class RTorrent:
|
|||||||
""" Create a new rTorrent connection """
|
""" Create a new rTorrent connection """
|
||||||
rpc_prefix = None
|
rpc_prefix = None
|
||||||
|
|
||||||
def __init__(self, url, username=None, password=None,
|
def __init__(self, uri, username=None, password=None,
|
||||||
verify=False, sp=HTTPServerProxy, sp_kwargs={}):
|
verify=False, sp=None, sp_kwargs=None):
|
||||||
self.url = url # : From X{__init__(self, url)}
|
self.uri = uri # : From X{__init__(self, url)}
|
||||||
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.sp = sp
|
|
||||||
self.sp_kwargs = sp_kwargs
|
self.schema = urllib.splittype(uri)[0]
|
||||||
|
|
||||||
|
if sp:
|
||||||
|
self.sp = sp
|
||||||
|
elif self.schema in ['http', 'https']:
|
||||||
|
self.sp = HTTPServerProxy
|
||||||
|
elif self.schema == 'scgi':
|
||||||
|
self.sp = SCGIServerProxy
|
||||||
|
else:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
self.sp_kwargs = sp_kwargs or {}
|
||||||
|
|
||||||
self.torrents = [] # : List of L{Torrent} instances
|
self.torrents = [] # : List of L{Torrent} instances
|
||||||
self._rpc_methods = [] # : List of rTorrent RPC methods
|
self._rpc_methods = [] # : List of rTorrent RPC methods
|
||||||
@@ -62,12 +77,16 @@ class RTorrent:
|
|||||||
def _get_conn(self):
|
def _get_conn(self):
|
||||||
"""Get ServerProxy instance"""
|
"""Get ServerProxy instance"""
|
||||||
if self.username is not None and self.password is not None:
|
if self.username is not None and self.password is not None:
|
||||||
|
if self.schema == 'scgi':
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
return self.sp(
|
return self.sp(
|
||||||
self.url,
|
self.uri,
|
||||||
transport=BasicAuthTransport(self.username, self.password),
|
transport=BasicAuthTransport(self.username, self.password),
|
||||||
**self.sp_kwargs
|
**self.sp_kwargs
|
||||||
)
|
)
|
||||||
return self.sp(self.url, **self.sp_kwargs)
|
|
||||||
|
return self.sp(self.uri, **self.sp_kwargs)
|
||||||
|
|
||||||
def _verify_conn(self):
|
def _verify_conn(self):
|
||||||
# check for rpc methods that should be available
|
# check for rpc methods that should be available
|
||||||
@@ -96,6 +115,11 @@ class RTorrent:
|
|||||||
|
|
||||||
return self._client_version_tuple
|
return self._client_version_tuple
|
||||||
|
|
||||||
|
def _update_rpc_methods(self):
|
||||||
|
self._rpc_methods = self._get_conn().system.listMethods()
|
||||||
|
|
||||||
|
return self._rpc_methods
|
||||||
|
|
||||||
def _get_rpc_methods(self):
|
def _get_rpc_methods(self):
|
||||||
""" Get list of raw RPC commands
|
""" Get list of raw RPC commands
|
||||||
|
|
||||||
@@ -103,10 +127,7 @@ class RTorrent:
|
|||||||
@rtype: list
|
@rtype: list
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._rpc_methods == []:
|
return(self._rpc_methods or self._update_rpc_methods())
|
||||||
self._rpc_methods = self._get_conn().system.listMethods()
|
|
||||||
|
|
||||||
return(self._rpc_methods)
|
|
||||||
|
|
||||||
def get_torrents(self, view="main"):
|
def get_torrents(self, view="main"):
|
||||||
"""Get list of all torrents in specified view
|
"""Get list of all torrents in specified view
|
||||||
@@ -298,6 +319,8 @@ class RTorrent:
|
|||||||
assert view is not None, "view parameter required on non-persistent groups"
|
assert view is not None, "view parameter required on non-persistent groups"
|
||||||
p.group.insert('', name, view)
|
p.group.insert('', name, view)
|
||||||
|
|
||||||
|
self._update_rpc_methods()
|
||||||
|
|
||||||
def get_group(self, name):
|
def get_group(self, name):
|
||||||
assert name is not None, "group name required"
|
assert name is not None, "group name required"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2013 Dean Gardiner, <gardiner91@gmail.com>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from base64 import encodestring
|
||||||
|
import string
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAuthTransport(xmlrpclib.Transport):
|
||||||
|
def __init__(self, username=None, password=None):
|
||||||
|
xmlrpclib.Transport.__init__(self)
|
||||||
|
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def send_auth(self, h):
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
h.putheader('AUTHORIZATION', "Basic %s" % string.replace(
|
||||||
|
encodestring("%s:%s" % (self.username, self.password)),
|
||||||
|
"\012", ""
|
||||||
|
))
|
||||||
|
|
||||||
|
def single_request(self, host, handler, request_body, verbose=0):
|
||||||
|
# issue XML-RPC request
|
||||||
|
|
||||||
|
h = self.make_connection(host)
|
||||||
|
if verbose:
|
||||||
|
h.set_debuglevel(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.send_request(h, handler, request_body)
|
||||||
|
self.send_host(h, host)
|
||||||
|
self.send_user_agent(h)
|
||||||
|
self.send_auth(h)
|
||||||
|
self.send_content(h, request_body)
|
||||||
|
|
||||||
|
response = h.getresponse(buffering=True)
|
||||||
|
if response.status == 200:
|
||||||
|
self.verbose = verbose
|
||||||
|
return self.parse_response(response)
|
||||||
|
except xmlrpclib.Fault:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
#discard any response data and raise exception
|
||||||
|
if response.getheader("content-length", 0):
|
||||||
|
response.read()
|
||||||
|
raise xmlrpclib.ProtocolError(
|
||||||
|
host + handler,
|
||||||
|
response.status, response.reason,
|
||||||
|
response.msg,
|
||||||
|
)
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# rtorrent_xmlrpc
|
||||||
|
# (c) 2011 Roger Que <alerante@bellsouth.net>
|
||||||
|
#
|
||||||
|
# Modified portions:
|
||||||
|
# (c) 2013 Dean Gardiner <gardiner91@gmail.com>
|
||||||
|
#
|
||||||
|
# Python module for interacting with rtorrent's XML-RPC interface
|
||||||
|
# directly over SCGI, instead of through an HTTP server intermediary.
|
||||||
|
# Inspired by Glenn Washburn's xmlrpc2scgi.py [1], but subclasses the
|
||||||
|
# built-in xmlrpclib classes so that it is compatible with features
|
||||||
|
# such as MultiCall objects.
|
||||||
|
#
|
||||||
|
# [1] <http://libtorrent.rakshasa.no/wiki/UtilsXmlrpc2scgi>
|
||||||
|
#
|
||||||
|
# Usage: server = SCGIServerProxy('scgi://localhost:7000/')
|
||||||
|
# server = SCGIServerProxy('scgi:///path/to/scgi.sock')
|
||||||
|
# print server.system.listMethods()
|
||||||
|
# mc = xmlrpclib.MultiCall(server)
|
||||||
|
# mc.get_up_rate()
|
||||||
|
# mc.get_down_rate()
|
||||||
|
# print mc()
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the
|
||||||
|
# OpenSSL library under certain conditions as described in each
|
||||||
|
# individual source file, and distribute linked combinations
|
||||||
|
# including the two.
|
||||||
|
#
|
||||||
|
# You must obey the GNU General Public License in all respects for
|
||||||
|
# all of the code used other than OpenSSL. If you modify file(s)
|
||||||
|
# with this exception, you may extend this exception to your version
|
||||||
|
# of the file(s), but you are not obligated to do so. If you do not
|
||||||
|
# wish to do so, delete this exception statement from your version.
|
||||||
|
# If you delete this exception statement from all source files in the
|
||||||
|
# program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Portions based on Python's xmlrpclib:
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999-2002 by Secret Labs AB
|
||||||
|
# Copyright (c) 1999-2002 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# By obtaining, using, and/or copying this software and/or its
|
||||||
|
# associated documentation, you agree that you have read, understood,
|
||||||
|
# and will comply with the following terms and conditions:
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and distribute this software and
|
||||||
|
# its associated documentation for any purpose and without fee is
|
||||||
|
# hereby granted, provided that the above copyright notice appears in
|
||||||
|
# all copies, and that both that copyright notice and this permission
|
||||||
|
# notice appear in supporting documentation, and that the name of
|
||||||
|
# Secret Labs AB or the author not be used in advertising or publicity
|
||||||
|
# pertaining to distribution of the software without specific, written
|
||||||
|
# prior permission.
|
||||||
|
#
|
||||||
|
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||||
|
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||||
|
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||||
|
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||||
|
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||||
|
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||||
|
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||||
|
# OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import httplib
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import urllib
|
||||||
|
import xmlrpclib
|
||||||
|
import errno
|
||||||
|
|
||||||
|
|
||||||
|
class SCGITransport(xmlrpclib.Transport):
|
||||||
|
# Added request() from Python 2.7 xmlrpclib here to backport to Python 2.6
|
||||||
|
def request(self, host, handler, request_body, verbose=0):
|
||||||
|
#retry request once if cached connection has gone cold
|
||||||
|
for i in (0, 1):
|
||||||
|
try:
|
||||||
|
return self.single_request(host, handler, request_body, verbose)
|
||||||
|
except socket.error, e:
|
||||||
|
if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE):
|
||||||
|
raise
|
||||||
|
except httplib.BadStatusLine: #close after we sent request
|
||||||
|
if i:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def single_request(self, host, handler, request_body, verbose=0):
|
||||||
|
# Add SCGI headers to the request.
|
||||||
|
headers = {'CONTENT_LENGTH': str(len(request_body)), 'SCGI': '1'}
|
||||||
|
header = '\x00'.join(('%s\x00%s' % item for item in headers.iteritems())) + '\x00'
|
||||||
|
header = '%d:%s' % (len(header), header)
|
||||||
|
request_body = '%s,%s' % (header, request_body)
|
||||||
|
|
||||||
|
sock = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if host:
|
||||||
|
host, port = urllib.splitport(host)
|
||||||
|
addrinfo = socket.getaddrinfo(host, int(port), socket.AF_INET,
|
||||||
|
socket.SOCK_STREAM)
|
||||||
|
sock = socket.socket(*addrinfo[0][:3])
|
||||||
|
sock.connect(addrinfo[0][4])
|
||||||
|
else:
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
sock.connect(handler)
|
||||||
|
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
sock.send(request_body)
|
||||||
|
return self.parse_response(sock.makefile())
|
||||||
|
finally:
|
||||||
|
if sock:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
def parse_response(self, response):
|
||||||
|
p, u = self.getparser()
|
||||||
|
|
||||||
|
response_body = ''
|
||||||
|
while True:
|
||||||
|
data = response.read(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
response_body += data
|
||||||
|
|
||||||
|
# Remove SCGI headers from the response.
|
||||||
|
response_header, response_body = re.split(r'\n\s*?\n', response_body,
|
||||||
|
maxsplit=1)
|
||||||
|
|
||||||
|
if self.verbose:
|
||||||
|
print 'body:', repr(response_body)
|
||||||
|
|
||||||
|
p.feed(response_body)
|
||||||
|
p.close()
|
||||||
|
|
||||||
|
return u.close()
|
||||||
|
|
||||||
|
|
||||||
|
class SCGIServerProxy(xmlrpclib.ServerProxy):
|
||||||
|
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
||||||
|
allow_none=False, use_datetime=False):
|
||||||
|
type, uri = urllib.splittype(uri)
|
||||||
|
if type not in ('scgi'):
|
||||||
|
raise IOError('unsupported XML-RPC protocol')
|
||||||
|
self.__host, self.__handler = urllib.splithost(uri)
|
||||||
|
if not self.__handler:
|
||||||
|
self.__handler = '/'
|
||||||
|
|
||||||
|
if transport is None:
|
||||||
|
transport = SCGITransport(use_datetime=use_datetime)
|
||||||
|
self.__transport = transport
|
||||||
|
|
||||||
|
self.__encoding = encoding
|
||||||
|
self.__verbose = verbose
|
||||||
|
self.__allow_none = allow_none
|
||||||
|
|
||||||
|
def __close(self):
|
||||||
|
self.__transport.close()
|
||||||
|
|
||||||
|
def __request(self, methodname, params):
|
||||||
|
# call a method on the remote server
|
||||||
|
|
||||||
|
request = xmlrpclib.dumps(params, methodname, encoding=self.__encoding,
|
||||||
|
allow_none=self.__allow_none)
|
||||||
|
|
||||||
|
response = self.__transport.request(
|
||||||
|
self.__host,
|
||||||
|
self.__handler,
|
||||||
|
request,
|
||||||
|
verbose=self.__verbose
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(response) == 1:
|
||||||
|
response = response[0]
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
"<SCGIServerProxy for %s%s>" %
|
||||||
|
(self.__host, self.__handler)
|
||||||
|
)
|
||||||
|
|
||||||
|
__str__ = __repr__
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
# magic method dispatcher
|
||||||
|
return xmlrpclib._Method(self.__request, name)
|
||||||
|
|
||||||
|
# note: to call a remote object with an non-standard name, use
|
||||||
|
# result getattr(server, "strange-python-name")(args)
|
||||||
|
|
||||||
|
def __call__(self, attr):
|
||||||
|
"""A workaround to get special attributes on the ServerProxy
|
||||||
|
without interfering with the magic __getattr__
|
||||||
|
"""
|
||||||
|
if attr == "close":
|
||||||
|
return self.__close
|
||||||
|
elif attr == "transport":
|
||||||
|
return self.__transport
|
||||||
|
raise AttributeError("Attribute %r not found" % (attr,))
|
||||||
@@ -17,66 +17,16 @@
|
|||||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
from base64 import encodestring
|
|
||||||
import httplib
|
|
||||||
import inspect
|
|
||||||
import string
|
|
||||||
|
|
||||||
|
import inspect
|
||||||
import rtorrent
|
import rtorrent
|
||||||
import re
|
import re
|
||||||
from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\
|
from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\
|
||||||
safe_repr
|
safe_repr
|
||||||
from rtorrent.err import RTorrentVersionError, MethodError
|
from rtorrent.err import MethodError
|
||||||
from rtorrent.compat import xmlrpclib
|
from rtorrent.compat import xmlrpclib
|
||||||
|
|
||||||
|
|
||||||
class BasicAuthTransport(xmlrpclib.Transport):
|
|
||||||
def __init__(self, username=None, password=None):
|
|
||||||
xmlrpclib.Transport.__init__(self)
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
def send_auth(self, h):
|
|
||||||
if self.username is not None and self.password is not None:
|
|
||||||
h.putheader('AUTHORIZATION', "Basic %s" % string.replace(
|
|
||||||
encodestring("%s:%s" % (self.username, self.password)),
|
|
||||||
"\012", ""
|
|
||||||
))
|
|
||||||
|
|
||||||
def single_request(self, host, handler, request_body, verbose=0):
|
|
||||||
# issue XML-RPC request
|
|
||||||
|
|
||||||
h = self.make_connection(host)
|
|
||||||
if verbose:
|
|
||||||
h.set_debuglevel(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.send_request(h, handler, request_body)
|
|
||||||
self.send_host(h, host)
|
|
||||||
self.send_user_agent(h)
|
|
||||||
self.send_auth(h)
|
|
||||||
self.send_content(h, request_body)
|
|
||||||
|
|
||||||
response = h.getresponse(buffering=True)
|
|
||||||
if response.status == 200:
|
|
||||||
self.verbose = verbose
|
|
||||||
return self.parse_response(response)
|
|
||||||
except xmlrpclib.Fault:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
self.close()
|
|
||||||
raise
|
|
||||||
|
|
||||||
#discard any response data and raise exception
|
|
||||||
if (response.getheader("content-length", 0)):
|
|
||||||
response.read()
|
|
||||||
raise xmlrpclib.ProtocolError(
|
|
||||||
host + handler,
|
|
||||||
response.status, response.reason,
|
|
||||||
response.msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_varname(rpc_call):
|
def get_varname(rpc_call):
|
||||||
"""Transform rpc method into variable name.
|
"""Transform rpc method into variable name.
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,17 @@ class Torrent:
|
|||||||
|
|
||||||
self.directory = m.call()[-1]
|
self.directory = m.call()[-1]
|
||||||
|
|
||||||
|
def set_directory_base(self, d):
|
||||||
|
"""Modify base download directory
|
||||||
|
|
||||||
|
@note: Needs to stop torrent in order to change the directory.
|
||||||
|
Also doesn't restart after directory is set, that must be called
|
||||||
|
separately.
|
||||||
|
"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.try_stop")
|
||||||
|
self.multicall_add(m, "d.set_directory_base", d)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start the torrent"""
|
"""Start the torrent"""
|
||||||
m = rtorrent.rpc.Multicall(self)
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
|||||||
Reference in New Issue
Block a user