diff --git a/couchpotato/api.py b/couchpotato/api.py
index 091de42a..e86b127f 100644
--- a/couchpotato/api.py
+++ b/couchpotato/api.py
@@ -110,6 +110,7 @@ class ApiHandler(RequestHandler):
if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
+ self.set_header("Content-Type", "text/javascript")
elif isinstance(result, tuple) and result[0] == 'redirect':
self.redirect(result[1])
else:
diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py
index b04e6898..026a56c6 100755
--- a/couchpotato/core/downloaders/rtorrent/__init__.py
+++ b/couchpotato/core/downloaders/rtorrent/__init__.py
@@ -23,6 +23,8 @@ config = [{
{
'name': 'url',
'default': 'http://localhost:80/RPC2',
+ 'description': 'XML-RPC Endpoint URI. Usually scgi://localhost:5000 '
+ 'or http://localhost:80/RPC2'
},
{
'name': 'username',
@@ -38,7 +40,7 @@ config = [{
{
'name': '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',
@@ -48,14 +50,6 @@ config = [{
'type': 'bool',
'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',
'label': 'Remove files',
@@ -64,6 +58,14 @@ config = [{
'advanced': True,
'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',
'type': 'bool',
diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py
index caf64d52..c8c323f9 100755
--- a/couchpotato/core/downloaders/rtorrent/main.py
+++ b/couchpotato/core/downloaders/rtorrent/main.py
@@ -71,7 +71,7 @@ class rTorrent(Downloader):
group.set_command()
group.disable()
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 True
diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
index d93c9417..15f9936d 100644
--- a/couchpotato/core/helpers/variable.py
+++ b/couchpotato/core/helpers/variable.py
@@ -1,3 +1,4 @@
+import collections
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
from couchpotato.core.logger import CPLog
import hashlib
@@ -145,9 +146,9 @@ def getImdb(txt, check_inside = False, multiple = False):
return False
-def tryInt(s):
+def tryInt(s, default=0):
try: return int(s)
- except: return 0
+ except: return default
def tryFloat(s):
try:
@@ -163,6 +164,11 @@ def natsortKey(s):
def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b))
+def toIterable(value):
+ if isinstance(value, collections.Iterable):
+ return value
+ return [value]
+
def getTitle(library_dict):
try:
try:
diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py
index 1cef967b..e6a249d5 100644
--- a/couchpotato/core/media/__init__.py
+++ b/couchpotato/core/media/__init__.py
@@ -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.settings.model import Media
class MediaBase(Plugin):
_type = None
+ default_dict = {
+ 'profile': {'types': {'quality': {}}},
+ 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
+ 'library': {'titles': {}, 'files':{}},
+ 'files': {},
+ 'status': {},
+ 'category': {},
+ }
+
def initType(self):
addEvent('media.types', self.getType)
def getType(self):
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
diff --git a/couchpotato/core/media/_base/media/__init__.py b/couchpotato/core/media/_base/media/__init__.py
new file mode 100644
index 00000000..a9693a3d
--- /dev/null
+++ b/couchpotato/core/media/_base/media/__init__.py
@@ -0,0 +1,6 @@
+from .main import MediaPlugin
+
+def start():
+ return MediaPlugin()
+
+config = []
diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py
new file mode 100644
index 00000000..87afb82a
--- /dev/null
+++ b/couchpotato/core/media/_base/media/main.py
@@ -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)
diff --git a/couchpotato/core/media/_base/search/__init__.py b/couchpotato/core/media/_base/search/__init__.py
new file mode 100644
index 00000000..4b2eae27
--- /dev/null
+++ b/couchpotato/core/media/_base/search/__init__.py
@@ -0,0 +1,6 @@
+from .main import Search
+
+def start():
+ return Search()
+
+config = []
diff --git a/couchpotato/core/media/_base/search/main.py b/couchpotato/core/media/_base/search/main.py
new file mode 100644
index 00000000..81897b5f
--- /dev/null
+++ b/couchpotato/core/media/_base/search/main.py
@@ -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))
diff --git a/couchpotato/core/media/movie/_base/static/search.css b/couchpotato/core/media/_base/search/static/search.css
similarity index 81%
rename from couchpotato/core/media/movie/_base/static/search.css
rename to couchpotato/core/media/_base/search/static/search.css
index 80c18153..57210d68 100644
--- a/couchpotato/core/media/movie/_base/static/search.css
+++ b/couchpotato/core/media/_base/search/static/search.css
@@ -129,13 +129,13 @@
overflow-x: hidden;
}
- .movie_result {
+ .media_result {
overflow: hidden;
height: 50px;
position: relative;
}
- .movie_result .options {
+ .media_result .options {
position: absolute;
height: 100%;
top: 0;
@@ -147,48 +147,48 @@
border-radius: 0;
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;
}
- .movie_result .options > div {
+ .media_result .options > div {
border: 0;
}
- .movie_result .options .thumbnail {
+ .media_result .options .thumbnail {
vertical-align: middle;
}
- .movie_result .options select {
+ .media_result .options select {
vertical-align: middle;
display: inline-block;
margin-right: 10px;
}
- .movie_result .options select[name=title] { width: 170px; }
- .movie_result .options select[name=profile] { width: 90px; }
- .movie_result .options select[name=category] { width: 80px; }
+ .media_result .options select[name=title] { width: 170px; }
+ .media_result .options select[name=profile] { width: 90px; }
+ .media_result .options select[name=category] { width: 80px; }
@media all and (max-width: 480px) {
- .movie_result .options select[name=title] { width: 90px; }
- .movie_result .options select[name=profile] { width: 50px; }
- .movie_result .options select[name=category] { width: 50px; }
+ .media_result .options select[name=title] { width: 90px; }
+ .media_result .options select[name=profile] { width: 50px; }
+ .media_result .options select[name=category] { width: 50px; }
}
- .movie_result .options .button {
+ .media_result .options .button {
vertical-align: middle;
display: inline-block;
}
- .movie_result .options .message {
+ .media_result .options .message {
height: 100%;
font-size: 20px;
color: #fff;
line-height: 20px;
}
- .movie_result .data {
+ .media_result .data {
position: absolute;
height: 100%;
top: 0;
@@ -199,20 +199,20 @@
border-top: 1px solid rgba(255,255,255, 0.08);
transition: all .4s cubic-bezier(0.9,0,0.1,1);
}
- .movie_result .data.open {
+ .media_result .data.open {
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;
bottom: 2px;
left: 14px;
font-size: 11px;
}
- .movie_result .thumbnail {
+ .media_result .thumbnail {
width: 34px;
min-height: 100%;
display: block;
@@ -220,7 +220,7 @@
vertical-align: top;
}
- .movie_result .info {
+ .media_result .info {
position: absolute;
top: 20%;
left: 15px;
@@ -228,7 +228,7 @@
vertical-align: middle;
}
- .movie_result .info h2 {
+ .media_result .info h2 {
margin: 0;
font-weight: normal;
font-size: 20px;
@@ -240,7 +240,7 @@
width: 100%;
}
- .movie_result .info h2 .title {
+ .media_result .info h2 .title {
display: block;
margin: 0;
text-overflow: ellipsis;
@@ -253,7 +253,7 @@
width: 88%;
}
- .movie_result .info h2 .year {
+ .media_result .info h2 .year {
padding: 0 5px;
text-align: center;
position: absolute;
@@ -271,7 +271,7 @@
}
.search_form .mask,
-.movie_result .mask {
+.media_result .mask {
position: absolute;
height: 100%;
width: 100%;
diff --git a/couchpotato/core/media/_base/search/static/search.js b/couchpotato/core/media/_base/search/static/search.js
new file mode 100644
index 00000000..470dcf0b
--- /dev/null
+++ b/couchpotato/core/media/_base/search/static/search.js
@@ -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();
+ }
+
+});
\ No newline at end of file
diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py
index 934a1472..f22072c7 100644
--- a/couchpotato/core/media/_base/searcher/main.py
+++ b/couchpotato/core/media/_base/searcher/main.py
@@ -2,12 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
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.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 inspect import ismethod, isfunction
+from sqlalchemy.exc import InterfaceError
import datetime
import re
import time
@@ -23,7 +24,10 @@ class Searcher(SearcherBase):
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
addEvent('searcher.correct_year', self.correctYear)
addEvent('searcher.correct_name', self.correctName)
+ addEvent('searcher.correct_words', self.correctWords)
addEvent('searcher.download', self.download)
+ addEvent('searcher.search', self.search)
+ addEvent('searcher.create_releases', self.createReleases)
addApiView('searcher.full_search', self.searchAllView, docs = {
'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()
if rls:
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
if isinstance(download_result, dict):
@@ -102,14 +105,20 @@ class Searcher(SearcherBase):
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
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)
- 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.last_edit = int(time.time())
db.commit()
except:
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:
log.error('Failed marking movie finished: %s', traceback.format_exc())
@@ -120,6 +129,75 @@ class Searcher(SearcherBase):
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):
download_protocols = fireEvent('download.enabled_protocols', merge = True)
@@ -224,5 +302,49 @@ class Searcher(SearcherBase):
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):
pass
diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py
index 310b4e92..6012013e 100644
--- a/couchpotato/core/media/movie/_base/main.py
+++ b/couchpotato/core/media/movie/_base/main.py
@@ -6,7 +6,7 @@ from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
mergeDicts
from couchpotato.core.logger import CPLog
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
from couchpotato.environment import Env
from sqlalchemy.orm import joinedload_all
@@ -19,14 +19,7 @@ log = CPLog(__name__)
class MovieBase(MovieTypeBase):
- default_dict = {
- 'profile': {'types': {'quality': {}}},
- 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {},
- 'status': {},
- 'category': {},
- }
+ _type = 'movie'
def __init__(self):
@@ -34,17 +27,6 @@ class MovieBase(MovieTypeBase):
super(MovieBase, self).__init__()
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 = {
'desc': 'List movies in wanted list',
'params': {
@@ -66,12 +48,6 @@ class MovieBase(MovieTypeBase):
'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.add', self.addView, docs = {
'desc': 'Add new movie to the wanted list',
@@ -103,34 +79,6 @@ class MovieBase(MovieTypeBase):
addEvent('movie.list', self.list)
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):
movie = self.get(id) if id else None
@@ -147,9 +95,9 @@ class MovieBase(MovieTypeBase):
imdb_id = getImdb(str(movie_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:
- m = db.query(Movie).filter_by(id = movie_id).first()
+ m = db.query(Media).filter_by(id = movie_id).first()
results = None
if m:
@@ -169,20 +117,20 @@ class MovieBase(MovieTypeBase):
release_status = [release_status]
# query movie ids
- q = db.query(Movie) \
- .with_entities(Movie.id) \
- .group_by(Movie.id)
+ q = db.query(Media) \
+ .with_entities(Media.id) \
+ .group_by(Media.id)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(status) > 1)
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
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 = [s.get('id') for s in statuses]
@@ -191,7 +139,7 @@ class MovieBase(MovieTypeBase):
# Only join when searching / ordering
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)
# Add search filters
@@ -242,13 +190,13 @@ class MovieBase(MovieTypeBase):
releases_count[release.movie_id] += 1
# Get main movie data
- q2 = db.query(Movie) \
+ q2 = db.query(Media) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
- q2 = q2.filter(Movie.id.in_(movie_ids))
+ q2 = q2.filter(Media.id.in_(movie_ids))
results = q2.all()
@@ -291,14 +239,14 @@ class MovieBase(MovieTypeBase):
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
- q = db.query(Movie)
+ q = db.query(Media)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
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
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 = [s.get('id') for s in statuses]
- q = q.join(Movie.releases) \
+ q = q.join(Media.releases) \
.filter(Release.status_id.in_(statuses))
q = q.join(Library, LibraryTitle) \
@@ -367,28 +315,6 @@ class MovieBase(MovieTypeBase):
'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):
cache_key = u'%s/%s' % (__name__, simplifyString(q))
@@ -438,12 +364,12 @@ class MovieBase(MovieTypeBase):
cat_id = params.get('category_id')
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
do_search = False
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
if not m:
- m = Movie(
+ m = Media(
library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.get('id')),
status_id = status_id if status_id else status_active.get('id'),
@@ -500,15 +426,12 @@ class MovieBase(MovieTypeBase):
db.expire_all()
return movie_dict
-
def addView(self, **kwargs):
-
- movie_dict = self.add(params = kwargs)
+ add_dict = self.add(params = kwargs)
return {
- 'success': True,
- 'added': True if movie_dict else False,
- 'movie': movie_dict,
+ 'success': True if add_dict else False,
+ 'movie': add_dict,
}
def edit(self, id = '', **kwargs):
@@ -520,7 +443,7 @@ class MovieBase(MovieTypeBase):
ids = splitString(id)
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:
continue
@@ -567,7 +490,7 @@ class MovieBase(MovieTypeBase):
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:
deleted = False
if delete_from == 'all':
@@ -617,7 +540,7 @@ class MovieBase(MovieTypeBase):
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:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
@@ -638,24 +561,3 @@ class MovieBase(MovieTypeBase):
db.commit()
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
diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js
index 341d2348..aaa8be12 100644
--- a/couchpotato/core/media/movie/_base/static/list.js
+++ b/couchpotato/core/media/movie/_base/static/list.js
@@ -422,7 +422,7 @@ var MovieList = new Class({
var self = this;
var ids = self.getSelectedMovies()
- Api.request('movie.refresh', {
+ Api.request('media.refresh', {
'data': {
'id': ids.join(','),
}
diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js
index 9dd6bdfe..0e9e5bf1 100644
--- a/couchpotato/core/media/movie/_base/static/movie.actions.js
+++ b/couchpotato/core/media/movie/_base/static/movie.actions.js
@@ -706,7 +706,7 @@ MA.Refresh = new Class({
var self = this;
(e).preventDefault();
- Api.request('movie.refresh', {
+ Api.request('media.refresh', {
'data': {
'id': self.movie.get('id')
}
diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css
index c72eb136..c013bd80 100644
--- a/couchpotato/core/media/movie/_base/static/movie.css
+++ b/couchpotato/core/media/movie/_base/static/movie.css
@@ -436,7 +436,7 @@
}
.movies .data .quality .seeding { background-color: #0a6819; }
.movies .data .quality .finish {
- background-image: url('../images/sprite.png');
+ background-image: url('../../images/sprite.png');
background-repeat: no-repeat;
background-position: 0 2px;
padding-left: 14px;
@@ -992,7 +992,7 @@
}
.movies .empty_wanted {
- background-image: url('../images/emptylist.png');
+ background-image: url('../../images/emptylist.png');
background-position: 80% 0;
height: 750px;
width: 100%;
diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js
index 7332381b..bb6f8ad2 100644
--- a/couchpotato/core/media/movie/_base/static/search.js
+++ b/couchpotato/core/media/movie/_base/static/search.js
@@ -1,189 +1,4 @@
-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 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({
+Block.Search.MovieItem = new Class({
Implements: [Options, Events],
@@ -201,7 +16,7 @@ Block.Search.Item = new Class({
var self = this,
info = self.info;
- self.el = new Element('div.movie_result', {
+ self.el = new Element('div.media_result', {
'id': info.imdb
}).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py
index b08e7532..09612252 100644
--- a/couchpotato/core/media/movie/searcher/main.py
+++ b/couchpotato/core/media/movie/searcher/main.py
@@ -1,16 +1,14 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
-from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
-from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
- possibleTitles, getImdb
+from couchpotato.core.helpers.encoding import simplifyString
+from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
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 datetime import date
-from sqlalchemy.exc import InterfaceError
import random
import re
import time
@@ -29,9 +27,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
addEvent('movie.searcher.all', self.searchAll)
addEvent('movie.searcher.all_view', self.searchAllView)
addEvent('movie.searcher.single', self.single)
- addEvent('movie.searcher.correct_movie', self.correctMovie)
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
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 = {
'desc': 'Marks the snatched results as ignored and try the next best release',
@@ -74,8 +73,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
db = get_session()
- movies = db.query(Movie).filter(
- Movie.status.has(identifier = 'active')
+ movies = db.query(Media).filter(
+ Media.status.has(identifier = 'active')
).all()
random.shuffle(movies)
@@ -117,6 +116,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
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
try:
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']))
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
- results = []
- for search_protocol in search_protocols:
- 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:
+ results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
+ if len(results) == 0:
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
- 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
# 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'])
- 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:
+ for nzb in 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):
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
continue
@@ -265,7 +222,11 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
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)
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']))
return False
- movie_name = getTitle(movie['library'])
- movie_words = re.split('\W+', simplifyString(movie_name))
- 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']))
+ # Check for required and ignored words
+ if not fireEvent('searcher.correct_words', nzb['name'], media, single = True):
return False
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# 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']))
return False
@@ -347,23 +272,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
return True
# Check if nzb contains imdb link
- if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
+ if getImdb(nzb.get('description', '')) == media['library']['identifier']:
return True
- for raw_title in movie['library']['titles']:
+ for raw_title in media['library']['titles']:
for movie_title in possibleTitles(raw_title['title']):
movie_words = re.split('\W+', simplifyString(movie_title))
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
# 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
# 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
- 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
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())
return False
+ def getSearchTitle(self, media):
+ if media['type'] == 'movie':
+ return getTitle(media['library'])
+
class SearchSetupError(Exception):
pass
diff --git a/couchpotato/core/plugins/suggestion/__init__.py b/couchpotato/core/media/movie/suggestion/__init__.py
similarity index 100%
rename from couchpotato/core/plugins/suggestion/__init__.py
rename to couchpotato/core/media/movie/suggestion/__init__.py
diff --git a/couchpotato/core/plugins/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py
similarity index 93%
rename from couchpotato/core/plugins/suggestion/main.py
rename to couchpotato/core/media/movie/suggestion/main.py
index eb31d26e..f29281ea 100644
--- a/couchpotato/core/plugins/suggestion/main.py
+++ b/couchpotato/core/media/movie/suggestion/main.py
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import splitString
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 sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_
@@ -29,9 +29,9 @@ class Suggestion(Plugin):
if not movies or len(movies) == 0:
db = get_session()
- active_movies = db.query(Movie) \
+ active_movies = db.query(Media) \
.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]
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)
db = get_session()
- active_movies = db.query(Movie) \
+ active_movies = db.query(Media) \
.join(Library) \
.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.extend(seen)
diff --git a/couchpotato/core/plugins/suggestion/static/suggest.css b/couchpotato/core/media/movie/suggestion/static/suggest.css
similarity index 52%
rename from couchpotato/core/plugins/suggestion/static/suggest.css
rename to couchpotato/core/media/movie/suggestion/static/suggest.css
index c321ca28..99d108d3 100644
--- a/couchpotato/core/plugins/suggestion/static/suggest.css
+++ b/couchpotato/core/media/movie/suggestion/static/suggest.css
@@ -5,31 +5,31 @@
height: 40px;
}
-.suggestions .movie_result {
+.suggestions .media_result {
display: inline-block;
width: 33.333%;
height: 150px;
}
@media all and (max-width: 960px) {
- .suggestions .movie_result {
+ .suggestions .media_result {
width: 50%;
}
}
@media all and (max-width: 600px) {
- .suggestions .movie_result {
+ .suggestions .media_result {
width: 100%;
}
}
- .suggestions .movie_result .data {
+ .suggestions .media_result .data {
left: 100px;
background: #4e5969;
border: none;
}
- .suggestions .movie_result .data .info {
+ .suggestions .media_result .data .info {
top: 15px;
left: 15px;
right: 15px;
@@ -37,32 +37,32 @@
overflow: hidden;
}
- .suggestions .movie_result .data .info h2 {
+ .suggestions .media_result .data .info h2 {
white-space: normal;
max-height: 120px;
font-size: 18px;
line-height: 18px;
}
- .suggestions .movie_result .data .info .rating,
- .suggestions .movie_result .data .info .genres,
- .suggestions .movie_result .data .info .year {
+ .suggestions .media_result .data .info .rating,
+ .suggestions .media_result .data .info .genres,
+ .suggestions .media_result .data .info .year {
position: static;
display: block;
padding: 0;
opacity: .6;
}
- .suggestions .movie_result .data .info .year {
+ .suggestions .media_result .data .info .year {
margin: 10px 0 0;
}
- .suggestions .movie_result .data .info .rating {
+ .suggestions .media_result .data .info .rating {
font-size: 20px;
float: right;
margin-top: -20px;
}
- .suggestions .movie_result .data .info .rating:before {
+ .suggestions .media_result .data .info .rating:before {
content: "\e031";
font-family: 'Elusive-Icons';
font-size: 14px;
@@ -70,25 +70,25 @@
vertical-align: bottom;
}
- .suggestions .movie_result .data .info .genres {
+ .suggestions .media_result .data .info .genres {
font-size: 11px;
font-style: italic;
text-align: right;
}
- .suggestions .movie_result .data {
+ .suggestions .media_result .data {
cursor: default;
}
- .suggestions .movie_result .options {
+ .suggestions .media_result .options {
left: 100px;
}
- .suggestions .movie_result .options select[name=title] { width: 100%; }
- .suggestions .movie_result .options select[name=profile] { width: 100%; }
- .suggestions .movie_result .options select[name=category] { width: 100%; }
+ .suggestions .media_result .options select[name=title] { width: 100%; }
+ .suggestions .media_result .options select[name=profile] { width: 100%; }
+ .suggestions .media_result .options select[name=category] { width: 100%; }
- .suggestions .movie_result .button {
+ .suggestions .media_result .button {
position: absolute;
margin: 2px 0 0 0;
right: 15px;
@@ -96,25 +96,25 @@
}
- .suggestions .movie_result .thumbnail {
+ .suggestions .media_result .thumbnail {
width: 100px;
}
- .suggestions .movie_result .actions {
+ .suggestions .media_result .actions {
position: absolute;
bottom: 10px;
right: 10px;
display: none;
width: 140px;
}
- .suggestions .movie_result:hover .actions {
+ .suggestions .media_result:hover .actions {
display: block;
}
- .suggestions .movie_result .data.open .actions {
+ .suggestions .media_result .data.open .actions {
display: none;
}
- .suggestions .movie_result .actions a {
+ .suggestions .media_result .actions a {
margin-left: 10px;
vertical-align: middle;
}
diff --git a/couchpotato/core/plugins/suggestion/static/suggest.js b/couchpotato/core/media/movie/suggestion/static/suggest.js
similarity index 94%
rename from couchpotato/core/plugins/suggestion/static/suggest.js
rename to couchpotato/core/media/movie/suggestion/static/suggest.js
index 40fe53b9..8664b0be 100644
--- a/couchpotato/core/plugins/suggestion/static/suggest.js
+++ b/couchpotato/core/media/movie/suggestion/static/suggest.js
@@ -17,7 +17,7 @@ var SuggestList = new Class({
'click:relay(a.delete)': function(e, el){
(e).stop();
- $(el).getParent('.movie_result').destroy();
+ $(el).getParent('.media_result').destroy();
Api.request('suggestion.ignore', {
'data': {
@@ -30,7 +30,7 @@ var SuggestList = new Class({
'click:relay(a.eye-open)': function(e, el){
(e).stop();
- $(el).getParent('.movie_result').destroy();
+ $(el).getParent('.media_result').destroy();
Api.request('suggestion.ignore', {
'data': {
@@ -65,7 +65,7 @@ var SuggestList = new Class({
Object.each(json.suggestions, function(movie){
- var m = new Block.Search.Item(movie, {
+ var m = new Block.Search.MovieItem(movie, {
'onAdded': function(){
self.afterAdded(m, movie)
}
diff --git a/couchpotato/core/notifications/plex/client.py b/couchpotato/core/notifications/plex/client.py
new file mode 100644
index 00000000..b873518e
--- /dev/null
+++ b/couchpotato/core/notifications/plex/client.py
@@ -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)
diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py
index 19ca670d..ce25c8f0 100755
--- a/couchpotato/core/notifications/plex/main.py
+++ b/couchpotato/core/notifications/plex/main.py
@@ -1,183 +1,64 @@
-from couchpotato.core.event import addEvent
-from couchpotato.core.helpers.encoding import tryUrlencode
-from couchpotato.core.helpers.variable import cleanHost
+from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
-from datetime import datetime
-from urlparse import urlparse
-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
+from .client import PlexClientHTTP, PlexClientJSON
+from .server import PlexServer
log = CPLog(__name__)
class Plex(Notification):
- client_update_time = 5 * 60
http_time_between_calls = 0
def __init__(self):
super(Plex, self).__init__()
- self.clients = {}
- self.clients_updated = None
+ self.server = PlexServer(self)
+
+ self.client_protocols = {
+ 'http': PlexClientHTTP(self),
+ 'json': PlexClientJSON(self)
+ }
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 = {}):
if self.isDisabled(): return
- log.info('Sending notification to Plex')
+ return self.server.refresh()
- source_type = ['movie']
- base_url = '%s/library/sections' % self.createHost(self.conf('media_server'), port = 32400)
- refresh_url = '%s/%%s/refresh' % base_url
+ def getClientNames(self):
+ return [
+ x.strip().lower()
+ for x in self.conf('clients').split(',')
+ ]
- try:
- sections_xml = self.urlopen(base_url)
- xml_sections = minidom.parseString(sections_xml)
- sections = xml_sections.getElementsByTagName('Directory')
+ def notifyClients(self, message, client_names):
+ success = True
- for s in sections:
- if s.getAttribute('type') in source_type:
- url = refresh_url % s.getAttribute('key')
- x = self.urlopen(url)
+ for client_name in client_names:
- except:
- log.error('Plex library update failed for %s, Media Server not running: %s',
- (self.conf('media_server'), traceback.format_exc(1)))
- return False
+ client_success = False
+ client = self.server.clients.get(client_name)
- return True
+ if client and client['found']:
+ client_success = fireEvent('notify.plex.notifyClient', client, message, single = True)
- def sendHTTP(self, command, client):
- url = 'http://%s:%s/xbmcCmds/xbmcHttp/?%s' % (
- client['address'],
- client['port'],
- tryUrlencode(command)
- )
+ if not client_success:
+ if self.server.staleClients() or not client:
+ log.info('Failed to send notification to client "%s". '
+ 'Client list is stale, updating the client list and retrying.', client_name)
+ 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:
- self.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 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 notify(self, message = '', data = {}, listener = None):
+ return self.notifyClients(message, self.getClientNames())
def test(self, **kwargs):
@@ -185,24 +66,12 @@ class Plex(Notification):
log.info('Sending test to %s', test_type)
- success = self.notify(
+ notify_success = self.notify(
message = self.test_message,
data = {},
- listener = 'test',
- force = True
+ listener = 'test'
)
- success2 = self.addToLibrary()
- return {
- 'success': success or success2
- }
+ refresh_success = self.addToLibrary()
- 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
+ return {'success': notify_success or refresh_success}
diff --git a/couchpotato/core/notifications/plex/server.py b/couchpotato/core/notifications/plex/server.py
new file mode 100644
index 00000000..b66db8fe
--- /dev/null
+++ b/couchpotato/core/notifications/plex/server.py
@@ -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
diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py
index d13a74a3..87cd0ea4 100644
--- a/couchpotato/core/plugins/category/main.py
+++ b/couchpotato/core/plugins/category/main.py
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
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__)
@@ -113,7 +113,7 @@ class CategoryPlugin(Plugin):
def removeFromMovie(self, category_id):
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:
for movie in movies:
diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py
index f006ac41..4f4d85ab 100644
--- a/couchpotato/core/plugins/dashboard/main.py
+++ b/couchpotato/core/plugins/dashboard/main.py
@@ -4,7 +4,7 @@ from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog
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
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import asc, or_
@@ -50,12 +50,12 @@ class Dashboard(Plugin):
# Get all active movies
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
- q = db.query(Movie) \
+ q = db.query(Media) \
.join(Library) \
- .outerjoin(Movie.releases) \
- .filter(Movie.status_id == active_status.get('id')) \
- .with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
- .group_by(Movie.id) \
+ .outerjoin(Media.releases) \
+ .filter(Media.status_id == active_status.get('id')) \
+ .with_entities(Media.id, Media.profile_id, Library.info, Library.year) \
+ .group_by(Media.id) \
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
if not random:
@@ -101,11 +101,11 @@ class Dashboard(Plugin):
if len(movie_ids) > 0:
# Get all movie information
- movies_raw = db.query(Movie) \
+ movies_raw = db.query(Media) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('files')) \
- .filter(Movie.id.in_(movie_ids)) \
+ .filter(Media.id.in_(movie_ids)) \
.all()
# Create dict by movie id
diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py
index 68ab9360..9ff3ead2 100644
--- a/couchpotato/core/plugins/profile/main.py
+++ b/couchpotato/core/plugins/profile/main.py
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
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
log = CPLog(__name__)
@@ -38,7 +38,7 @@ class ProfilePlugin(Plugin):
active_status = fireEvent('status.get', 'active', single = True)
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:
default_profile = self.default()
diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py
index e8d1815f..9df23d96 100644
--- a/couchpotato/core/plugins/release/main.py
+++ b/couchpotato/core/plugins/release/main.py
@@ -5,12 +5,12 @@ from couchpotato.core.helpers.encoding import ss
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
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.sql.expression import and_, or_
import os
-import traceback
import time
+import traceback
log = CPLog(__name__)
@@ -50,6 +50,38 @@ class Release(Plugin):
addEvent('release.clean', self.clean)
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):
db = get_session()
@@ -60,9 +92,9 @@ class Release(Plugin):
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
# 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:
- movie = Movie(
+ movie = Media(
library_id = group['library'].get('id'),
profile_id = 0,
status_id = done_status.get('id')
diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
index 921b3e1e..c8f6b37f 100755
--- a/couchpotato/core/plugins/renamer/__init__.py
+++ b/couchpotato/core/plugins/renamer/__init__.py
@@ -28,6 +28,7 @@ rename_options = {
'cd': 'CD number (cd1)',
'cd_nr': 'Just the cd nr. (1)',
'mpaa': 'MPAA Rating',
+ 'category': 'Category label',
},
}
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
index 28d5eef7..5f5473e4 100755
--- a/couchpotato/core/plugins/renamer/main.py
+++ b/couchpotato/core/plugins/renamer/main.py
@@ -134,7 +134,7 @@ class Renamer(Plugin):
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info))
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')
file_name = self.conf('file_name')
@@ -173,7 +173,12 @@ class Renamer(Plugin):
# Overwrite destination when set in category
destination = self.conf('to')
+ category_label = ''
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':
destination = movie.category.destination
log.debug('Setting category destination for "%s": %s' % (movie_title, destination))
@@ -217,6 +222,7 @@ class Renamer(Plugin):
'cd': '',
'cd_nr': '',
'mpaa': library['info'].get('mpaa', ''),
+ 'category': category_label,
}
for file_type in group['files']:
diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
index 58627093..316e81af 100644
--- a/couchpotato/core/plugins/scanner/main.py
+++ b/couchpotato/core/plugins/scanner/main.py
@@ -5,7 +5,7 @@ from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \
splitString
from couchpotato.core.logger import CPLog
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 guessit import guess_movie_info
from subliminal.videos import Video
@@ -415,7 +415,7 @@ class Scanner(Plugin):
if not group['library']:
log.error('Unable to determine movie: %s', group['identifiers'])
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
processed_movies[identifier] = group
@@ -830,19 +830,21 @@ class Scanner(Plugin):
def findYear(self, text):
# Search year inside () or [] first
- matches = re.search('(\(|\[)(?P19[0-9]{2}|20[0-9]{2})(\]|\))', text)
+ matches = re.findall('(\(|\[)(?P19[0-9]{2}|20[0-9]{2})(\]|\))', text)
if matches:
- return matches.group('year')
+ return matches[-1][1]
# Search normal
- matches = re.search('(?P19[0-9]{2}|20[0-9]{2})', text)
+ matches = re.findall('(?P19[0-9]{2}|20[0-9]{2})', text)
if matches:
- return matches.group('year')
+ return matches[-1]
return ''
def getReleaseNameYear(self, release_name, file_name = None):
+ release_name = release_name.strip(' .-_')
+
# Use guessit first
guess = {}
if file_name:
@@ -860,7 +862,7 @@ class Scanner(Plugin):
cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
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
year = self.findYear(year_str)
if year:
@@ -870,19 +872,21 @@ class Scanner(Plugin):
if year: # Split name on year
try:
- movie_name = cleaned.split(year).pop(0).strip()
- cp_guess = {
- 'name': movie_name,
- 'year': int(year),
- }
+ movie_name = cleaned.rsplit(year, 1).pop(0).strip()
+ if movie_name:
+ cp_guess = {
+ 'name': movie_name,
+ 'year': int(year),
+ }
except:
pass
- else: # Split name on multiple spaces
+
+ if not cp_guess: # Split name on multiple spaces
try:
movie_name = cleaned.split(' ').pop(0).strip()
cp_guess = {
'name': movie_name,
- 'year': int(year),
+ 'year': int(year) if movie_name[:4] != year else 0,
}
except:
pass
diff --git a/couchpotato/core/plugins/userscript/static/userscript.css b/couchpotato/core/plugins/userscript/static/userscript.css
index d08953a6..d8161014 100644
--- a/couchpotato/core/plugins/userscript/static/userscript.css
+++ b/couchpotato/core/plugins/userscript/static/userscript.css
@@ -14,25 +14,25 @@
padding: 20px;
}
- .page.userscript .movie_result {
+ .page.userscript .media_result {
height: 140px;
}
- .page.userscript .movie_result .thumbnail {
+ .page.userscript .media_result .thumbnail {
width: 90px;
}
- .page.userscript .movie_result .options {
+ .page.userscript .media_result .options {
left: 90px;
padding: 54px 15px;
}
- .page.userscript .movie_result .year {
+ .page.userscript .media_result .year {
display: none;
}
- .page.userscript .movie_result .options select[name="title"] {
+ .page.userscript .media_result .options select[name="title"] {
width: 190px;
}
- .page.userscript .movie_result .options select[name="profile"] {
+ .page.userscript .media_result .options select[name="profile"] {
width: 70px;
}
diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js
index 2aeb7b5f..11daa068 100644
--- a/couchpotato/core/plugins/userscript/static/userscript.js
+++ b/couchpotato/core/plugins/userscript/static/userscript.js
@@ -34,7 +34,7 @@ Page.Userscript = new Class({
if(json.error)
self.frame.set('html', json.error);
else {
- var item = new Block.Search.Item(json.movie);
+ var item = new Block.Search.MovieItem(json.movie);
self.frame.adopt(item);
item.showOptions();
}
diff --git a/couchpotato/core/providers/automation/flixster/main.py b/couchpotato/core/providers/automation/flixster/main.py
index 46dcfba3..7fd2f717 100644
--- a/couchpotato/core/providers/automation/flixster/main.py
+++ b/couchpotato/core/providers/automation/flixster/main.py
@@ -1,7 +1,6 @@
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
-import json
log = CPLog(__name__)
@@ -40,7 +39,7 @@ class Flixster(Automation):
if not enablers[index]:
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:
movies.append({'title': movie['movie']['title'], 'year': movie['movie']['year'] })
diff --git a/couchpotato/core/providers/automation/itunes/main.py b/couchpotato/core/providers/automation/itunes/main.py
index 8e352370..eb68e348 100644
--- a/couchpotato/core/providers/automation/itunes/main.py
+++ b/couchpotato/core/providers/automation/itunes/main.py
@@ -16,9 +16,6 @@ class ITunes(Automation, RSS):
def getIMDBids(self):
- if self.isDisabled():
- return
-
movies = []
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py
index e6a9cb00..60420a13 100644
--- a/couchpotato/core/providers/base.py
+++ b/couchpotato/core/providers/base.py
@@ -1,4 +1,5 @@
from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
possibleTitles, getTitle
from couchpotato.core.logger import CPLog
@@ -7,6 +8,7 @@ from couchpotato.environment import Env
from urlparse import urlparse
import cookielib
import json
+import logging
import re
import time
import traceback
@@ -15,7 +17,6 @@ import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
-
class MultiProvider(Plugin):
def __init__(self):
@@ -63,13 +64,17 @@ class Provider(Plugin):
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', {})))
data = self.getCache(cache_key, url, **kwargs)
if data:
try:
+ data = data.strip()
+ if decode_from:
+ data = data.decode(decode_from)
+
return json.loads(data)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
@@ -279,8 +284,7 @@ class ResultList(list):
new_result = self.fillResult(result)
- is_correct_movie = fireEvent('movie.searcher.correct_movie',
- nzb = new_result, movie = self.movie, quality = self.quality,
+ is_correct_movie = fireEvent('searcher.correct_release', new_result, self.movie, self.quality,
imdb_results = self.kwargs.get('imdb_results', False), single = True)
if is_correct_movie and new_result['id'] not in self.result_ids:
diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py
index 835cce04..091557d2 100644
--- a/couchpotato/core/providers/info/_modifier/main.py
+++ b/couchpotato/core/providers/info/_modifier/main.py
@@ -32,9 +32,32 @@ class MovieResultModifier(Plugin):
}
def __init__(self):
+ addEvent('result.modify.info.search', self.returnByType)
addEvent('result.modify.movie.search', self.combineOnIMDB)
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):
temp = {}
diff --git a/couchpotato/core/providers/info/omdbapi/main.py b/couchpotato/core/providers/info/omdbapi/main.py
index 87bb0a73..f05c7cb3 100755
--- a/couchpotato/core/providers/info/omdbapi/main.py
+++ b/couchpotato/core/providers/info/omdbapi/main.py
@@ -84,6 +84,7 @@ class OMDBAPI(MovieProvider):
year = tryInt(movie.get('Year', ''))
movie_data = {
+ 'type': 'movie',
'via_imdb': True,
'titles': [movie.get('Title')] if movie.get('Title') else [],
'original_title': movie.get('Title'),
diff --git a/couchpotato/core/providers/info/themoviedb/main.py b/couchpotato/core/providers/info/themoviedb/main.py
index 376ddad0..87579a0c 100644
--- a/couchpotato/core/providers/info/themoviedb/main.py
+++ b/couchpotato/core/providers/info/themoviedb/main.py
@@ -11,6 +11,7 @@ log = CPLog(__name__)
class TheMovieDb(MovieProvider):
def __init__(self):
+ addEvent('info.search', self.search, priority = 2)
addEvent('movie.search', self.search, priority = 2)
addEvent('movie.info', self.getInfo, priority = 2)
addEvent('movie.info_by_tmdb', self.getInfo)
@@ -103,6 +104,7 @@ class TheMovieDb(MovieProvider):
year = None
movie_data = {
+ 'type': 'movie',
'via_tmdb': True,
'tmdb_id': movie.id,
'titles': [toUnicode(movie.title)],
diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py
index 02ffcfdc..bd1b6c32 100644
--- a/couchpotato/core/providers/nzb/newznab/main.py
+++ b/couchpotato/core/providers/nzb/newznab/main.py
@@ -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.variable import cleanHost, splitString, tryInt
from couchpotato.core.logger import CPLog
@@ -83,7 +83,7 @@ class Newznab(NZBProvider, RSS):
results.append({
'id': nzb_id,
'provider_extra': urlparse(host['host']).hostname or host['host'],
- 'name': name,
+ 'name': toUnicode(name),
'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
diff --git a/couchpotato/core/providers/torrent/yify/main.py b/couchpotato/core/providers/torrent/yify/main.py
index 4c059463..60b2f9b1 100644
--- a/couchpotato/core/providers/torrent/yify/main.py
+++ b/couchpotato/core/providers/torrent/yify/main.py
@@ -23,9 +23,9 @@ class Yify(TorrentProvider):
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'):
try:
diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index f39544bc..8601c2b4 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -78,6 +78,7 @@ class Movie(Entity):
such as trailers, nfo, thumbnails"""
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)
status = ManyToOne('Status')
@@ -86,6 +87,7 @@ class Movie(Entity):
releases = OneToMany('Release', cascade = 'all, delete-orphan')
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
+Media = Movie # Compat tv branch
class Library(Entity):
""""""
diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
index dcd0f7bd..59fac34b 100644
--- a/couchpotato/static/scripts/couchpotato.js
+++ b/couchpotato/static/scripts/couchpotato.js
@@ -32,7 +32,7 @@
self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self));
// Check if device is touchenabled
- self.touch_device = 'ontouchstart' in document.documentElement;
+ self.touch_device = 'ontouchstart' in window || navigator.msMaxTouchPoints;
if(self.touch_device)
self.c.addClass('touch_enabled');
diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js
index f9313353..b4326ae5 100644
--- a/couchpotato/static/scripts/page/about.js
+++ b/couchpotato/static/scripts/page/about.js
@@ -106,7 +106,7 @@ var AboutSettingTab = new Class({
new Element('div.donate', {
'html':
'Or support me via:' +
- ''
+ ''
})
);
diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js
index b93db5bd..9967ff41 100644
--- a/couchpotato/static/scripts/page/home.js
+++ b/couchpotato/static/scripts/page/home.js
@@ -52,7 +52,7 @@ Page.Home = new Class({
})
),
'filter': {
- 'release_status': 'snatched,available'
+ 'release_status': 'snatched,seeding,missing,available,downloaded'
},
'limit': null,
'onLoaded': function(){
diff --git a/init/ubuntu b/init/ubuntu
index 7f770a67..1d2eb57a 100644
--- a/init/ubuntu
+++ b/init/ubuntu
@@ -20,6 +20,8 @@ else
echo "/etc/default/couchpotato not found using default settings.";
fi
+. /lib/lsb/init-functions
+
# Script name
NAME=couchpotato
diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py
index b6ff73a0..683ef1c7 100755
--- a/libs/rtorrent/__init__.py
+++ b/libs/rtorrent/__init__.py
@@ -17,18 +17,21 @@
# 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.
+import urllib
+import os.path
+import time
+import xmlrpclib
from rtorrent.common import find_torrent, \
is_valid_port, convert_version_tuple_to_str
from rtorrent.lib.torrentparser import TorrentParser
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.group import Group
-import os.path
import rtorrent.rpc # @UnresolvedImport
-import time
-import xmlrpclib
__version__ = "0.2.9"
__author__ = "Chris Lucas"
@@ -43,13 +46,25 @@ class RTorrent:
""" Create a new rTorrent connection """
rpc_prefix = None
- def __init__(self, url, username=None, password=None,
- verify=False, sp=HTTPServerProxy, sp_kwargs={}):
- self.url = url # : From X{__init__(self, url)}
+ def __init__(self, uri, username=None, password=None,
+ verify=False, sp=None, sp_kwargs=None):
+ self.uri = uri # : From X{__init__(self, url)}
+
self.username = username
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._rpc_methods = [] # : List of rTorrent RPC methods
@@ -62,12 +77,16 @@ class RTorrent:
def _get_conn(self):
"""Get ServerProxy instance"""
if self.username is not None and self.password is not None:
+ if self.schema == 'scgi':
+ raise NotImplementedError()
+
return self.sp(
- self.url,
+ self.uri,
transport=BasicAuthTransport(self.username, self.password),
**self.sp_kwargs
)
- return self.sp(self.url, **self.sp_kwargs)
+
+ return self.sp(self.uri, **self.sp_kwargs)
def _verify_conn(self):
# check for rpc methods that should be available
@@ -96,6 +115,11 @@ class RTorrent:
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):
""" Get list of raw RPC commands
@@ -103,10 +127,7 @@ class RTorrent:
@rtype: list
"""
- if self._rpc_methods == []:
- self._rpc_methods = self._get_conn().system.listMethods()
-
- return(self._rpc_methods)
+ return(self._rpc_methods or self._update_rpc_methods())
def get_torrents(self, view="main"):
"""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"
p.group.insert('', name, view)
+ self._update_rpc_methods()
+
def get_group(self, name):
assert name is not None, "group name required"
diff --git a/libs/rtorrent/lib/xmlrpc/basic_auth.py b/libs/rtorrent/lib/xmlrpc/basic_auth.py
new file mode 100644
index 00000000..20c02d9a
--- /dev/null
+++ b/libs/rtorrent/lib/xmlrpc/basic_auth.py
@@ -0,0 +1,73 @@
+#
+# Copyright (c) 2013 Dean Gardiner,
+#
+# 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,
+ )
diff --git a/libs/rtorrent/lib/xmlrpc/scgi.py b/libs/rtorrent/lib/xmlrpc/scgi.py
new file mode 100644
index 00000000..5ba61fa5
--- /dev/null
+++ b/libs/rtorrent/lib/xmlrpc/scgi.py
@@ -0,0 +1,219 @@
+#!/usr/bin/python
+
+# rtorrent_xmlrpc
+# (c) 2011 Roger Que
+#
+# Modified portions:
+# (c) 2013 Dean Gardiner
+#
+# 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]
+#
+# 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 (
+ "" %
+ (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,))
diff --git a/libs/rtorrent/rpc/__init__.py b/libs/rtorrent/rpc/__init__.py
index 034f4eef..116ca1c2 100755
--- a/libs/rtorrent/rpc/__init__.py
+++ b/libs/rtorrent/rpc/__init__.py
@@ -17,66 +17,16 @@
# 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 httplib
-import inspect
-import string
+import inspect
import rtorrent
import re
from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\
safe_repr
-from rtorrent.err import RTorrentVersionError, MethodError
+from rtorrent.err import MethodError
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):
"""Transform rpc method into variable name.
diff --git a/libs/rtorrent/torrent.py b/libs/rtorrent/torrent.py
index c610e368..bd6bb689 100755
--- a/libs/rtorrent/torrent.py
+++ b/libs/rtorrent/torrent.py
@@ -172,6 +172,17 @@ class Torrent:
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):
"""Start the torrent"""
m = rtorrent.rpc.Multicall(self)