Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2013-10-14 00:02:30 +02:00
52 changed files with 1312 additions and 779 deletions
+1
View File
@@ -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
+8 -2
View File
@@ -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:
+32 -1
View File
@@ -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))
@@ -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();
}
});
+127 -5
View File
@@ -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
+23 -121
View File
@@ -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', {
+33 -104
View File
@@ -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
@@ -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)
@@ -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;
} }
@@ -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)
+37 -168
View File
@@ -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
+2 -2
View File
@@ -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:
+8 -8
View File
@@ -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
+2 -2
View File
@@ -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()
+36 -4
View File
@@ -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',
}, },
} }
+7 -1
View File
@@ -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']:
+18 -14
View File
@@ -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'))]
+8 -4
View File
@@ -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:
+2
View File
@@ -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):
"""""" """"""
+1 -1
View File
@@ -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');
+1 -1
View File
@@ -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>'
}) })
); );
+1 -1
View File
@@ -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(){
+2
View File
@@ -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
View File
@@ -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"
+73
View File
@@ -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,
)
+219
View File
@@ -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,))
+2 -52
View File
@@ -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.
+11
View File
@@ -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)