From 8ca5c6257540441d9985eaad5da96722e9486646 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Oct 2013 15:52:25 +0200 Subject: [PATCH 01/45] YIFY use IMDB id for search. fix #2313 --- couchpotato/core/providers/torrent/yify/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/torrent/yify/main.py b/couchpotato/core/providers/torrent/yify/main.py index 4c059463..47fe310c 100644 --- a/couchpotato/core/providers/torrent/yify/main.py +++ b/couchpotato/core/providers/torrent/yify/main.py @@ -25,7 +25,7 @@ class Yify(TorrentProvider): def _searchOnTitle(self, title, 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: From 3925d4c215ba7946bab9b5673f9c9dc203a1ca47 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Oct 2013 21:23:09 +0200 Subject: [PATCH 02/45] Make search work for multiple media types --- .../core/media/_base/search/__init__.py | 6 ++ couchpotato/core/media/_base/search/main.py | 58 +++++++++++++++++++ couchpotato/core/media/movie/_base/main.py | 11 ---- .../core/media/movie/_base/static/search.js | 2 +- .../core/providers/info/_modifier/main.py | 23 ++++++++ .../core/providers/info/omdbapi/main.py | 1 + .../core/providers/info/themoviedb/main.py | 2 + 7 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 couchpotato/core/media/_base/search/__init__.py create mode 100644 couchpotato/core/media/_base/search/main.py 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..41f7f9da --- /dev/null +++ b/couchpotato/core/media/_base/search/main.py @@ -0,0 +1,58 @@ +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, + 'empty': bool, any movies returned or not, + 'results': object {'movie': [], 'show': []}, by media type found, +}"""} + }) + + 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/main.py b/couchpotato/core/media/movie/_base/main.py index 310b4e92..c4e58b27 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -34,17 +34,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': { diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js index 7332381b..45028a25 100644 --- a/couchpotato/core/media/movie/_base/static/search.js +++ b/couchpotato/core/media/movie/_base/static/search.js @@ -131,7 +131,7 @@ Block.Search = new Class({ if(!self.spinner) self.spinner = createSpinner(self.mask); - self.api_request = Api.request('movie.search', { + self.api_request = Api.request('search', { 'data': { 'q': q }, 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)], From bca4a2e241ab2694de4c49aed0c2b84b918dfa62 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Oct 2013 22:51:23 +0200 Subject: [PATCH 03/45] Move search item to movie folder --- couchpotato/core/media/_base/search/main.py | 5 +- .../_base => _base/search}/static/search.css | 48 ++--- .../core/media/_base/search/static/search.js | 188 +++++++++++++++++ .../core/media/movie/_base/static/movie.css | 4 +- .../core/media/movie/_base/static/search.js | 189 +----------------- .../plugins/suggestion/static/suggest.css | 48 ++--- .../core/plugins/suggestion/static/suggest.js | 6 +- 7 files changed, 246 insertions(+), 242 deletions(-) rename couchpotato/core/media/{movie/_base => _base/search}/static/search.css (81%) create mode 100644 couchpotato/core/media/_base/search/static/search.js diff --git a/couchpotato/core/media/_base/search/main.py b/couchpotato/core/media/_base/search/main.py index 41f7f9da..81897b5f 100644 --- a/couchpotato/core/media/_base/search/main.py +++ b/couchpotato/core/media/_base/search/main.py @@ -19,8 +19,9 @@ class Search(Plugin): }, 'return': {'type': 'object', 'example': """{ 'success': True, - 'empty': bool, any movies returned or not, - 'results': object {'movie': [], 'show': []}, by media type found, + 'movies': array, + 'show': array, + etc }"""} }) 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/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 45028a25..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('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/plugins/suggestion/static/suggest.css b/couchpotato/core/plugins/suggestion/static/suggest.css index c321ca28..99d108d3 100644 --- a/couchpotato/core/plugins/suggestion/static/suggest.css +++ b/couchpotato/core/plugins/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/plugins/suggestion/static/suggest.js index 40fe53b9..8664b0be 100644 --- a/couchpotato/core/plugins/suggestion/static/suggest.js +++ b/couchpotato/core/plugins/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) } From 10fe175ff56cffe23f1a082d0bcef794a553ad04 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Oct 2013 22:52:05 +0200 Subject: [PATCH 04/45] Move suggestions to movie folder --- couchpotato/core/{plugins => media/movie}/suggestion/__init__.py | 0 couchpotato/core/{plugins => media/movie}/suggestion/main.py | 0 .../core/{plugins => media/movie}/suggestion/static/suggest.css | 0 .../core/{plugins => media/movie}/suggestion/static/suggest.js | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename couchpotato/core/{plugins => media/movie}/suggestion/__init__.py (100%) rename couchpotato/core/{plugins => media/movie}/suggestion/main.py (100%) rename couchpotato/core/{plugins => media/movie}/suggestion/static/suggest.css (100%) rename couchpotato/core/{plugins => media/movie}/suggestion/static/suggest.js (100%) 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 100% rename from couchpotato/core/plugins/suggestion/main.py rename to couchpotato/core/media/movie/suggestion/main.py diff --git a/couchpotato/core/plugins/suggestion/static/suggest.css b/couchpotato/core/media/movie/suggestion/static/suggest.css similarity index 100% rename from couchpotato/core/plugins/suggestion/static/suggest.css rename to couchpotato/core/media/movie/suggestion/static/suggest.css diff --git a/couchpotato/core/plugins/suggestion/static/suggest.js b/couchpotato/core/media/movie/suggestion/static/suggest.js similarity index 100% rename from couchpotato/core/plugins/suggestion/static/suggest.js rename to couchpotato/core/media/movie/suggestion/static/suggest.js From 955814397a47dccdd2efcacf7646825d248f0f59 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Oct 2013 23:38:53 +0200 Subject: [PATCH 05/45] Revert "TorrentBytes login url change. fix #2317" This reverts commit 95d0dacd28b1905b8c452c079ddfc407d08cee50. --- couchpotato/core/providers/torrent/torrentbytes/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/torrent/torrentbytes/main.py b/couchpotato/core/providers/torrent/torrentbytes/main.py index 02b86abb..a5849a91 100644 --- a/couchpotato/core/providers/torrent/torrentbytes/main.py +++ b/couchpotato/core/providers/torrent/torrentbytes/main.py @@ -12,7 +12,7 @@ class TorrentBytes(TorrentProvider): urls = { 'test' : 'https://www.torrentbytes.net/', - 'login' : 'https://www.torrentbytes.net/login.php', + 'login' : 'https://www.torrentbytes.net/takelogin.php', 'login_check' : 'https://www.torrentbytes.net/inbox.php', 'detail' : 'https://www.torrentbytes.net/details.php?id=%s', 'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d', From bac305572689201d4024d8cfbc7cb57f585ecdab Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 08:45:45 +0200 Subject: [PATCH 06/45] Move media refresh to media plugin --- couchpotato/core/media/__init__.py | 33 ++++++++++++- .../core/media/_base/media/__init__.py | 6 +++ couchpotato/core/media/_base/media/main.py | 48 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 couchpotato/core/media/_base/media/__init__.py create mode 100644 couchpotato/core/media/_base/media/main.py 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..58fa378a --- /dev/null +++ b/couchpotato/core/media/_base/media/__init__.py @@ -0,0 +1,6 @@ +from .main import Media + +def start(): + return Media() + +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..2035cd8f --- /dev/null +++ b/couchpotato/core/media/_base/media/main.py @@ -0,0 +1,48 @@ +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 + +log = CPLog(__name__) + + +class Media(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) From b8ac093182617419e659953988b4af7ea373f338 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 08:48:38 +0200 Subject: [PATCH 07/45] Remove refresh from movie media Conflicts: couchpotato/core/media/movie/_base/main.py --- couchpotato/core/media/movie/_base/main.py | 67 ++----------------- .../core/media/movie/_base/static/list.js | 2 +- .../media/movie/_base/static/movie.actions.js | 2 +- 3 files changed, 6 insertions(+), 65 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index c4e58b27..494c460e 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -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): @@ -55,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', @@ -356,28 +343,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)) @@ -489,15 +454,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): @@ -627,24 +589,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') } From eabd2b6c412d3f800110dddffbdd95d6a6f5c54e Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 09:21:53 +0200 Subject: [PATCH 08/45] Rename mediaplugin --- couchpotato/core/media/_base/media/__init__.py | 4 ++-- couchpotato/core/media/_base/media/main.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/_base/media/__init__.py b/couchpotato/core/media/_base/media/__init__.py index 58fa378a..a9693a3d 100644 --- a/couchpotato/core/media/_base/media/__init__.py +++ b/couchpotato/core/media/_base/media/__init__.py @@ -1,6 +1,6 @@ -from .main import Media +from .main import MediaPlugin def start(): - return Media() + return MediaPlugin() config = [] diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 2035cd8f..286d4043 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -8,7 +8,7 @@ from couchpotato.core.media import MediaBase log = CPLog(__name__) -class Media(MediaBase): +class MediaPlugin(MediaBase): def __init__(self): From 32646d060882763651c4434b98ba20d79010bd7a Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 09:22:05 +0200 Subject: [PATCH 09/45] Use movie instaid of media model --- couchpotato/core/media/__init__.py | 6 +++--- couchpotato/core/media/_base/media/main.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index e6a249d5..cdf667c9 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,7 +1,7 @@ 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 +from couchpotato.core.settings.model import Movie class MediaBase(Plugin): @@ -27,7 +27,7 @@ class MediaBase(Plugin): def onComplete(): db = get_session() - media = db.query(Media).filter_by(id = id).first() + media = db.query(Movie).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() @@ -37,7 +37,7 @@ class MediaBase(Plugin): def notifyFront(): db = get_session() - media = db.query(Media).filter_by(id = media_id).first() + media = db.query(Movie).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() diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 286d4043..d0ddf113 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -4,6 +4,7 @@ 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 Movie log = CPLog(__name__) @@ -25,7 +26,7 @@ class MediaPlugin(MediaBase): db = get_session() for x in splitString(id): - media = db.query(Media).filter_by(id = x).first() + media = db.query(Movie).filter_by(id = x).first() if media: # Get current selected title From 107606ce659145e7a63945c542bc5f5b14b9fa10 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 09:57:17 +0200 Subject: [PATCH 10/45] Add tv branch column aliases --- couchpotato/core/settings/model.py | 2 ++ 1 file changed, 2 insertions(+) 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): """""" From d1c3f0c24134f3c6fbb742f0debf1cacdb903e35 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 09:57:36 +0200 Subject: [PATCH 11/45] Use Media for all Movie db actions --- couchpotato/core/media/__init__.py | 6 +-- couchpotato/core/media/_base/media/main.py | 4 +- couchpotato/core/media/_base/searcher/main.py | 4 +- couchpotato/core/media/movie/_base/main.py | 42 +++++++++---------- couchpotato/core/media/movie/searcher/main.py | 8 ++-- .../core/media/movie/suggestion/main.py | 10 ++--- couchpotato/core/plugins/category/main.py | 4 +- couchpotato/core/plugins/dashboard/main.py | 16 +++---- couchpotato/core/plugins/profile/main.py | 4 +- couchpotato/core/plugins/release/main.py | 8 ++-- couchpotato/core/plugins/scanner/main.py | 2 +- 11 files changed, 54 insertions(+), 54 deletions(-) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index cdf667c9..e6a249d5 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,7 +1,7 @@ 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 Movie +from couchpotato.core.settings.model import Media class MediaBase(Plugin): @@ -27,7 +27,7 @@ class MediaBase(Plugin): def onComplete(): db = get_session() - media = db.query(Movie).filter_by(id = id).first() + 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() @@ -37,7 +37,7 @@ class MediaBase(Plugin): def notifyFront(): db = get_session() - media = db.query(Movie).filter_by(id = media_id).first() + 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() diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index d0ddf113..87afb82a 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -4,7 +4,7 @@ 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 Movie +from couchpotato.core.settings.model import Media log = CPLog(__name__) @@ -26,7 +26,7 @@ class MediaPlugin(MediaBase): db = get_session() for x in splitString(id): - media = db.query(Movie).filter_by(id = x).first() + media = db.query(Media).filter_by(id = x).first() if media: # Get current selected title diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 934a1472..b6b36125 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -5,7 +5,7 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.variable import md5, getTitle 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 import datetime @@ -104,7 +104,7 @@ class Searcher(SearcherBase): 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() + 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() diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 494c460e..4f39733d 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 @@ -96,8 +96,8 @@ class MovieBase(MovieTypeBase): 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)) \ + movies = db.query(Media) \ + .filter(Media.status_id == done_status.get('id'), Media.last_edit < (now - week)) \ .all() for movie in movies: @@ -123,9 +123,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: @@ -145,20 +145,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] @@ -167,7 +167,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 @@ -218,13 +218,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() @@ -267,14 +267,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: @@ -282,7 +282,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) \ @@ -392,12 +392,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'), @@ -471,7 +471,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 @@ -518,7 +518,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': @@ -568,7 +568,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 diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index b08e7532..79d2ed84 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -7,7 +7,7 @@ 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.media.movie import MovieTypeBase -from couchpotato.core.settings.model import Movie, Release, ReleaseInfo +from couchpotato.core.settings.model import Media, Release, ReleaseInfo from couchpotato.environment import Env from datetime import date from sqlalchemy.exc import InterfaceError @@ -74,8 +74,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) @@ -182,7 +182,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): 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 diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py index eb31d26e..f29281ea 100644 --- a/couchpotato/core/media/movie/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/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..e6726635 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__) @@ -60,9 +60,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/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 58627093..afbfa741 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -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 From a0d2a64e57de05897708eb7c33e6751a36e8d610 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 21:51:34 +0200 Subject: [PATCH 12/45] Userscript didn't load properly --- couchpotato/core/plugins/userscript/static/userscript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); } From 75bda46f64bd79845eb3c8bfa0114c0abe2cd757 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 8 Oct 2013 21:53:03 +0200 Subject: [PATCH 13/45] Userscript styling fixes --- .../core/plugins/userscript/static/userscript.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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; } From 0a90ad5db7a5740d653164b3dc213408505b6ad1 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Wed, 9 Oct 2013 22:24:22 +1300 Subject: [PATCH 14/45] Updated rtorrent library to current master - scgi:// support --- libs/rtorrent/__init__.py | 41 +++-- libs/rtorrent/lib/xmlrpc/basic_auth.py | 73 +++++++++ libs/rtorrent/lib/xmlrpc/scgi.py | 201 +++++++++++++++++++++++++ libs/rtorrent/rpc/__init__.py | 54 +------ libs/rtorrent/torrent.py | 11 ++ 5 files changed, 317 insertions(+), 63 deletions(-) create mode 100644 libs/rtorrent/lib/xmlrpc/basic_auth.py create mode 100644 libs/rtorrent/lib/xmlrpc/scgi.py diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py index b6ff73a0..05add8d4 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 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..39866971 --- /dev/null +++ b/libs/rtorrent/lib/xmlrpc/scgi.py @@ -0,0 +1,201 @@ +#!/usr/bin/python + +# rtorrent_xmlrpc +# (c) 2011 Roger Que +# +# 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 re +import socket +import urllib +import xmlrpclib + + +class SCGITransport(xmlrpclib.Transport): + 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, 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) From a8087c8ce9c5c0b6afee7dbc3c28c28b11f4a810 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Wed, 9 Oct 2013 23:07:14 +1300 Subject: [PATCH 15/45] Updated rTorrent downloader options --- .../core/downloaders/rtorrent/__init__.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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', From a46241bb9fbf59c449bb78bcb457921c54b2e266 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 9 Oct 2013 16:36:13 +0200 Subject: [PATCH 16/45] Better year name guessing. #2323 --- couchpotato/core/plugins/scanner/main.py | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index afbfa741..08634b6c 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -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 From 5fd0253089e9961aae9de77bfbd2afcc0f5a1d4f Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 9 Oct 2013 16:37:16 +0200 Subject: [PATCH 17/45] Import Media, not Movie. fix #2320 --- couchpotato/core/plugins/scanner/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 08634b6c..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 From 99606e22d6ebc867ceca06dd4eaa24ab7940ab4e Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 9 Oct 2013 16:45:45 +0200 Subject: [PATCH 18/45] Make YIFY a imdbid search. fix #2323 --- couchpotato/core/providers/torrent/yify/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/torrent/yify/main.py b/couchpotato/core/providers/torrent/yify/main.py index 47fe310c..60b2f9b1 100644 --- a/couchpotato/core/providers/torrent/yify/main.py +++ b/couchpotato/core/providers/torrent/yify/main.py @@ -23,7 +23,7 @@ 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'] % (movie['library']['identifier'], quality['identifier'])) From df90ee0a55b07b8c604d12059b07ccdaca7031d4 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 10 Oct 2013 15:57:15 +1300 Subject: [PATCH 19/45] Updated rtorrent library - scgi fix for Python 2.6 --- libs/rtorrent/lib/xmlrpc/scgi.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libs/rtorrent/lib/xmlrpc/scgi.py b/libs/rtorrent/lib/xmlrpc/scgi.py index 39866971..88515e5c 100644 --- a/libs/rtorrent/lib/xmlrpc/scgi.py +++ b/libs/rtorrent/lib/xmlrpc/scgi.py @@ -3,6 +3,9 @@ # 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 @@ -78,13 +81,28 @@ # 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'} From b9f88f431b32599fdc7775c8883ae2b0f16d3091 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 11 Oct 2013 04:12:36 +1300 Subject: [PATCH 20/45] Updated rTorrent library and fixed call to MethodError.message (should be MethodError.msg) in _update_provider_group --- couchpotato/core/downloaders/rtorrent/main.py | 2 +- libs/rtorrent/lib/xmlrpc/scgi.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/libs/rtorrent/lib/xmlrpc/scgi.py b/libs/rtorrent/lib/xmlrpc/scgi.py index 88515e5c..5ba61fa5 100644 --- a/libs/rtorrent/lib/xmlrpc/scgi.py +++ b/libs/rtorrent/lib/xmlrpc/scgi.py @@ -115,7 +115,7 @@ class SCGITransport(xmlrpclib.Transport): try: if host: host, port = urllib.splitport(host) - addrinfo = socket.getaddrinfo(host, port, socket.AF_INET, + addrinfo = socket.getaddrinfo(host, int(port), socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(*addrinfo[0][:3]) sock.connect(addrinfo[0][4]) From 8587b9b7802ef30c3d5cc539631ec849d89956c4 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 11 Oct 2013 13:33:20 +1300 Subject: [PATCH 21/45] Updated rTorrent library - MethodError exceptions when calling group methods should be fixed. --- libs/rtorrent/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py index 05add8d4..683ef1c7 100755 --- a/libs/rtorrent/__init__.py +++ b/libs/rtorrent/__init__.py @@ -115,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 @@ -122,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 @@ -317,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" From 34c69786de37be4ac09b68536fdbb269788de638 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 12 Oct 2013 14:25:00 +1300 Subject: [PATCH 22/45] Merge base/movie searcher changes from branch 'tv' into develop --- couchpotato/core/helpers/variable.py | 10 +- couchpotato/core/media/_base/searcher/main.py | 134 +++++++++++++++++- couchpotato/core/media/movie/searcher/main.py | 127 ++++------------- 3 files changed, 164 insertions(+), 107 deletions(-) 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/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index b6b36125..4348f41a 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -2,11 +2,12 @@ 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 Media, Release, ReleaseInfo from couchpotato.environment import Env +from sqlalchemy.exc import InterfaceError from inspect import ismethod, isfunction import datetime import re @@ -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', @@ -60,7 +64,7 @@ class Searcher(SearcherBase): if downloader_enabled: - snatched_status, done_status, active_status = fireEvent('status.get', ['snatched', 'done', 'active'], single = True) + snatched_status = fireEvent('status.get', 'snatched', single = True) # Download movie to temp filedata = None @@ -79,7 +83,9 @@ 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) + + done_status = fireEvent('status.get', 'done', single = True) + rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id') # Save download-id info if returned if isinstance(download_result, dict): @@ -98,12 +104,20 @@ class Searcher(SearcherBase): # If renamer isn't used, mark movie done if not renamer_enabled: + active_status = fireEvent('status.get', 'active', single = True) + done_status = fireEvent('status.get', 'done', single = True) try: 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) + + # Mark release done + rls.status_id = done_status.get('id') + rls.last_edit = int(time.time()) + db.commit() + + # 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()) @@ -120,6 +134,74 @@ 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, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], 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, + 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 +306,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/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index 79d2ed84..d6c33675 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 Media, 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', @@ -167,64 +166,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) + 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(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 +218,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 +231,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 +268,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 +355,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 From 73d7d01ae4aeb3d3b65d5e764ff4c6126d744842 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 12 Oct 2013 15:10:26 +1300 Subject: [PATCH 23/45] Fixed ResultList.append call to 'movie.searcher.correct_movie' instead of 'searcher.correct_release' --- couchpotato/core/providers/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index e6a9cb00..2fa8c11d 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -1,3 +1,4 @@ +import logging from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \ possibleTitles, getTitle @@ -15,7 +16,6 @@ import xml.etree.ElementTree as XMLTree log = CPLog(__name__) - class MultiProvider(Plugin): def __init__(self): @@ -279,8 +279,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: From e96724beafe25b92ca5c815de24ce23457b7ee00 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 12 Oct 2013 15:11:46 +1300 Subject: [PATCH 24/45] Fix to MovieSearcher.single to set default media type as types aren't in develop yet. --- couchpotato/core/media/movie/searcher/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index d6c33675..63dcf761 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -116,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 'type' not in movie: + movie['type'] = 'movie' + # Find out search type try: if not search_protocols: From 8970e7fbba720fdc35cb005f01bd6a125eafd5c7 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 12 Oct 2013 15:24:06 +1300 Subject: [PATCH 25/45] Fix to Searcher.createReleases (media_id doesn't exist yet) --- couchpotato/core/media/_base/searcher/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 4348f41a..a96f8452 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -172,7 +172,8 @@ class Searcher(SearcherBase): if not rls: rls = Release( identifier = nzb_identifier, - media_id = media.get('id'), + movie_id = media.get('id'), + #media_id = media.get('id'), quality_id = quality_type.get('quality_id'), status_id = available_status.get('id') ) From 79fd5fe3321d230cee8649922af5eefe35d68c18 Mon Sep 17 00:00:00 2001 From: cicavey Date: Thu, 3 Oct 2013 23:05:25 -0400 Subject: [PATCH 26/45] Changed MIME type of JSONP requests to text/javascript --- couchpotato/api.py | 1 + 1 file changed, 1 insertion(+) 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: From 6d2889f88d2a9d02da3bba9dce8267c4b3e06c55 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 12 Oct 2013 13:28:44 +0200 Subject: [PATCH 27/45] Fix releases missing from Snatched&Available Fixes #1958 --- couchpotato/static/scripts/page/home.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(){ From 3a117b6077f3651094044ed705f1a9c5c77067cd Mon Sep 17 00:00:00 2001 From: mano3m Date: Sat, 12 Oct 2013 13:40:51 +0200 Subject: [PATCH 28/45] Make sure movies are removed from dashboard --- couchpotato/core/media/movie/_base/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 4f39733d..e430a65a 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -83,26 +83,26 @@ class MovieBase(MovieTypeBase): addEvent('app.load', self.cleanReleases) fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4) - def cleanReleases(self): + def cleanReleases(self): # Shouldnt this be part of the Release plugin, or the Dashboard plugin? 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) + done_status, available_status, snatched_status, downloaded_status = \ + fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded'], single = True) db = get_session() # get movies last_edit more than a week ago - movies = db.query(Media) \ + media = db.query(Media) \ .filter(Media.status_id == done_status.get('id'), Media.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')]: + for item in media: + for rel in item.releases: + if rel.status_id in [available_status.get('id'), snatched_status.get('id'), downloaded_status.get('id')]: fireEvent('release.delete', id = rel.id, single = True) db.expire_all() From d31ca2677eb20676604dd396de75bd1adbf315f3 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 13 Oct 2013 02:26:35 +1300 Subject: [PATCH 29/45] Cleaned up Plex notifications plugin. --- couchpotato/core/notifications/plex/client.py | 85 ++++++++ couchpotato/core/notifications/plex/main.py | 204 +++--------------- couchpotato/core/notifications/plex/server.py | 111 ++++++++++ 3 files changed, 230 insertions(+), 170 deletions(-) create mode 100644 couchpotato/core/notifications/plex/client.py create mode 100644 couchpotato/core/notifications/plex/server.py 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..3c127fee 100755 --- a/couchpotato/core/notifications/plex/main.py +++ b/couchpotato/core/notifications/plex/main.py @@ -1,183 +1,59 @@ -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 notifyClients(self, message, clients): + success = True - try: - sections_xml = self.urlopen(base_url) - xml_sections = minidom.parseString(sections_xml) - sections = xml_sections.getElementsByTagName('Directory') + while len(clients): + client = clients[0] - for s in sections: - if s.getAttribute('type') in source_type: - url = refresh_url % s.getAttribute('key') - x = self.urlopen(url) + success = fireEvent('notify.plex.notifyClient', client, message, single=True) - except: - log.error('Plex library update failed for %s, Media Server not running: %s', - (self.conf('media_server'), traceback.format_exc(1))) - return False + if success: + clients.pop(0) + else: + if self.server.staleClients(): + log.info('Failed to send notification to client "%s". ' + 'Client list is stale, updating the client list and retrying.', client['name']) + self.server.updateClients() + else: + log.warning('Failed to send notification to client %s, skipping this time', client['name']) + clients.pop(0) + success = False + break - return True + return success - def sendHTTP(self, command, client): - url = 'http://%s:%s/xbmcCmds/xbmcHttp/?%s' % ( - client['address'], - client['port'], - tryUrlencode(command) - ) - - headers = {} - - 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.server.clients.values()) def test(self, **kwargs): @@ -185,24 +61,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..67e4937b --- /dev/null +++ b/couchpotato/core/notifications/plex/server.py @@ -0,0 +1,111 @@ +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): + log.info('Searching for clients on Plex Media Server') + + self.clients = {} + + result = self.request('clients') + if not result: + return + + notify_clients = [ + x.strip().lower() + for x in self.plex.conf('clients').split(',') + ] + + found_clients = [ + c for c in result.findall('Server') + if c.get('name') and c.get('name').lower() in notify_clients + ] + + for client in found_clients: + name = client.get('name').lower() + + self.clients[name] = { + 'name': client.get('name'), + 'address': client.get('address'), + 'port': client.get('port'), + 'protocol': client.get('protocol', 'xbmchttp') + } + + notify_clients.remove(name) + + if len(notify_clients) > 0: + log.debug('Unable to find clients: %s', ', '.join(notify_clients)) + + 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 From efdf70acb288d8e71b5189e3ffa67c4ba29e6500 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 13 Oct 2013 02:52:55 +1300 Subject: [PATCH 30/45] When notifications fail to send the client list is automatically reloaded in case the client address has changed. --- couchpotato/core/notifications/plex/main.py | 35 ++++++++++++------- couchpotato/core/notifications/plex/server.py | 15 +++----- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py index 3c127fee..d3644d5e 100755 --- a/couchpotato/core/notifications/plex/main.py +++ b/couchpotato/core/notifications/plex/main.py @@ -29,31 +29,40 @@ class Plex(Notification): return self.server.refresh() - def notifyClients(self, message, clients): + def getClientNames(self): + return [ + x.strip().lower() + for x in self.conf('clients').split(',') + ] + + def notifyClients(self, message, client_names): success = True - while len(clients): - client = clients[0] + while len(client_names): + client_name = client_names[0] + client_success = False + client = self.server.clients.get(client_name) - success = fireEvent('notify.plex.notifyClient', client, message, single=True) + if client: + client_success = fireEvent('notify.plex.notifyClient', client, message, single=True) - if success: - clients.pop(0) - else: + if client_success: + client_names.pop(0) + + if not client_success: if self.server.staleClients(): log.info('Failed to send notification to client "%s". ' - 'Client list is stale, updating the client list and retrying.', client['name']) - self.server.updateClients() + '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']) - clients.pop(0) + log.warning('Failed to send notification to client %s, skipping this time', client_name) + client_names.pop(0) success = False - break return success def notify(self, message = '', data = {}, listener = None): - return self.notifyClients(message, self.server.clients.values()) + return self.notifyClients(message, self.getClientNames()) def test(self, **kwargs): diff --git a/couchpotato/core/notifications/plex/server.py b/couchpotato/core/notifications/plex/server.py index 67e4937b..4df6e9bb 100644 --- a/couchpotato/core/notifications/plex/server.py +++ b/couchpotato/core/notifications/plex/server.py @@ -44,7 +44,7 @@ class PlexServer(object): else: return data - def updateClients(self): + def updateClients(self, client_names): log.info('Searching for clients on Plex Media Server') self.clients = {} @@ -53,14 +53,9 @@ class PlexServer(object): if not result: return - notify_clients = [ - x.strip().lower() - for x in self.plex.conf('clients').split(',') - ] - found_clients = [ c for c in result.findall('Server') - if c.get('name') and c.get('name').lower() in notify_clients + if c.get('name') and c.get('name').lower() in client_names ] for client in found_clients: @@ -73,10 +68,10 @@ class PlexServer(object): 'protocol': client.get('protocol', 'xbmchttp') } - notify_clients.remove(name) + client_names.remove(name) - if len(notify_clients) > 0: - log.debug('Unable to find clients: %s', ', '.join(notify_clients)) + if len(client_names) > 0: + log.debug('Unable to find clients: %s', ', '.join(client_names)) self.last_clients_update = datetime.now() From bdeace8a68b4acd0f4b5c8a54de0f33c692bbb15 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 13 Oct 2013 03:00:52 +1300 Subject: [PATCH 31/45] New clients added that aren't in the current client cache now trigger a reload if the list isn't "stale" yet. --- couchpotato/core/notifications/plex/main.py | 4 ++-- couchpotato/core/notifications/plex/server.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py index d3644d5e..514b119e 100755 --- a/couchpotato/core/notifications/plex/main.py +++ b/couchpotato/core/notifications/plex/main.py @@ -43,14 +43,14 @@ class Plex(Notification): client_success = False client = self.server.clients.get(client_name) - if client: + if client and client['found']: client_success = fireEvent('notify.plex.notifyClient', client, message, single=True) if client_success: client_names.pop(0) if not client_success: - if self.server.staleClients(): + 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()) diff --git a/couchpotato/core/notifications/plex/server.py b/couchpotato/core/notifications/plex/server.py index 4df6e9bb..b66db8fe 100644 --- a/couchpotato/core/notifications/plex/server.py +++ b/couchpotato/core/notifications/plex/server.py @@ -58,11 +58,13 @@ class PlexServer(object): 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') @@ -70,6 +72,12 @@ class PlexServer(object): 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)) From 93bd75acc8d4710f79216a0986b31f91a1f2d6a4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 12 Oct 2013 23:12:45 +0200 Subject: [PATCH 32/45] Make iframe https --- couchpotato/static/scripts/page/about.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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:' + - '' + '' }) ); From 6dbdd4c0be0fabaea6399d4dd3dcb58e5363cb5c Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Tue, 8 Oct 2013 13:58:27 -0600 Subject: [PATCH 33/45] Load lsb init-functions so that status_of_proc is available --- init/ubuntu | 2 ++ 1 file changed, 2 insertions(+) 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 From f4c4f013da7451a6a96cfc9b9146889ce511b3b1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 13:44:26 +0200 Subject: [PATCH 34/45] Cleanup searcher and release checking --- couchpotato/core/media/_base/searcher/main.py | 17 ++++++----------- couchpotato/core/media/movie/searcher/main.py | 4 ++-- couchpotato/core/plugins/renamer/main.py | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index a96f8452..f22072c7 100644 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -7,8 +7,8 @@ from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.settings.model import Media, Release, ReleaseInfo from couchpotato.environment import Env -from sqlalchemy.exc import InterfaceError from inspect import ismethod, isfunction +from sqlalchemy.exc import InterfaceError import datetime import re import time @@ -64,7 +64,7 @@ class Searcher(SearcherBase): if downloader_enabled: - snatched_status = fireEvent('status.get', 'snatched', single = True) + snatched_status, done_status, active_status = fireEvent('status.get', ['snatched', 'done', 'active'], single = True) # Download movie to temp filedata = None @@ -84,9 +84,6 @@ class Searcher(SearcherBase): if rls: renamer_enabled = Env.setting('enabled', 'renamer') - done_status = fireEvent('status.get', 'done', single = True) - rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id') - # Save download-id info if returned if isinstance(download_result, dict): for key in download_result: @@ -104,8 +101,6 @@ class Searcher(SearcherBase): # If renamer isn't used, mark movie done if not renamer_enabled: - active_status = fireEvent('status.get', 'active', single = True) - done_status = fireEvent('status.get', 'done', single = True) try: if movie['status_id'] == active_status.get('id'): for profile_type in movie['profile']['types']: @@ -113,9 +108,7 @@ class Searcher(SearcherBase): log.info('Renamer disabled, marking movie as finished: %s', log_movie) # Mark release done - rls.status_id = done_status.get('id') - rls.last_edit = int(time.time()) - db.commit() + fireEvent('release.update_status', rls.id, status = done_status, single = True) # Mark movie done mvie = db.query(Media).filter_by(id = movie['id']).first() @@ -124,6 +117,8 @@ class Searcher(SearcherBase): 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()) @@ -158,7 +153,7 @@ class Searcher(SearcherBase): def createReleases(self, search_results, media, quality_type): - available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True) + available_status = fireEvent('status.get', ['available'], single = True) db = get_session() found_releases = [] diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index 63dcf761..09612252 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -117,7 +117,7 @@ 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 'type' not in movie: + if not movie.has_key('type'): movie['type'] = 'movie' # Find out search type @@ -170,7 +170,7 @@ 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 = fireEvent('searcher.search', search_protocols, movie, quality, single = True) + 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'])) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 28d5eef7..64690e5a 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') From 9bf01e3a0bfa8ea4a420caba0540730f7eb13ab2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 14:01:18 +0200 Subject: [PATCH 35/45] Plex endless loop when no clients connected --- couchpotato/core/notifications/plex/main.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py index 514b119e..ce25c8f0 100755 --- a/couchpotato/core/notifications/plex/main.py +++ b/couchpotato/core/notifications/plex/main.py @@ -38,16 +38,13 @@ class Plex(Notification): def notifyClients(self, message, client_names): success = True - while len(client_names): - client_name = client_names[0] + for client_name in client_names: + client_success = False client = self.server.clients.get(client_name) if client and client['found']: - client_success = fireEvent('notify.plex.notifyClient', client, message, single=True) - - if client_success: - client_names.pop(0) + client_success = fireEvent('notify.plex.notifyClient', client, message, single = True) if not client_success: if self.server.staleClients() or not client: @@ -56,7 +53,6 @@ class Plex(Notification): self.server.updateClients(self.getClientNames()) else: log.warning('Failed to send notification to client %s, skipping this time', client_name) - client_names.pop(0) success = False return success From 3be6389fbfd1ad9d65d5c36eefc8f5bd7ce1bfd3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 14:16:59 +0200 Subject: [PATCH 36/45] Use json in flixter --- couchpotato/core/providers/automation/flixster/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/couchpotato/core/providers/automation/flixster/main.py b/couchpotato/core/providers/automation/flixster/main.py index 46dcfba3..429f9fb7 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) for movie in data: movies.append({'title': movie['movie']['title'], 'year': movie['movie']['year'] }) From 65f039e9eda288460331e567dabf409ebaf7c8c7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 14:25:50 +0200 Subject: [PATCH 37/45] Make sure to untag downloading dir if it's completed. fix #2341 --- couchpotato/core/plugins/renamer/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 64690e5a..e0ece591 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -735,6 +735,10 @@ Remove it if you want it to be renamed (again, or at least let it try again) fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id) elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) + + # Remove the downloading tag + self.untagDir(item['folder'], 'downloading') + if self.statusInfoComplete(item): # If the release has been seeding, process now the seeding is done @@ -758,9 +762,6 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Set the release to snatched if it was missing before fireEvent('release.update_status', rel.id, status = snatched_status, single = True) - # Remove the downloading tag - self.untagDir(item['folder'], 'downloading') - # Scan and Allow the downloader to clean-up item.update({'pause': False, 'scan': True, 'process_complete': True}) scan_items.append(item) From 2b57bdcd03c79a814a6e44fe015080a28e6e4ee2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 15:17:39 +0200 Subject: [PATCH 38/45] Revert "Make sure to untag downloading dir if it's completed. fix #2341" This reverts commit 65f039e9eda288460331e567dabf409ebaf7c8c7. --- couchpotato/core/plugins/renamer/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index e0ece591..64690e5a 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -735,10 +735,6 @@ Remove it if you want it to be renamed (again, or at least let it try again) fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id) elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) - - # Remove the downloading tag - self.untagDir(item['folder'], 'downloading') - if self.statusInfoComplete(item): # If the release has been seeding, process now the seeding is done @@ -762,6 +758,9 @@ Remove it if you want it to be renamed (again, or at least let it try again) # Set the release to snatched if it was missing before fireEvent('release.update_status', rel.id, status = snatched_status, single = True) + # Remove the downloading tag + self.untagDir(item['folder'], 'downloading') + # Scan and Allow the downloader to clean-up item.update({'pause': False, 'scan': True, 'process_complete': True}) scan_items.append(item) From 4b0a5bdd9bc270a86355c662a84e4e32d82a7332 Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 13 Oct 2013 16:49:00 +0200 Subject: [PATCH 39/45] Move and fix cleanreleases --- couchpotato/core/media/movie/_base/main.py | 28 ------------------- couchpotato/core/plugins/release/main.py | 32 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index e430a65a..6012013e 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -79,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): # Shouldnt this be part of the Release plugin, or the Dashboard plugin? - - log.debug('Removing releases from dashboard') - - now = time.time() - week = 262080 - - done_status, available_status, snatched_status, downloaded_status = \ - fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded'], 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: - if rel.status_id in [available_status.get('id'), snatched_status.get('id'), downloaded_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 diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index e6726635..9df23d96 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -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() From c9d74188995d25a2ae394ab22de52e09d8229dfc Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 21:46:16 +0200 Subject: [PATCH 40/45] Force unicode name for newznab. fix #2347 --- couchpotato/core/providers/nzb/newznab/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From c772758683f5c96097122fb4c15673aa9d59f321 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 22:12:15 +0200 Subject: [PATCH 41/45] Add category to renamer replacements. fix #2283 --- couchpotato/core/plugins/renamer/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 64690e5a..5f5473e4 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -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']: From 3535f44db9d710c2a489ec99dc9e7b05e0f9fc0d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 22:12:27 +0200 Subject: [PATCH 42/45] No need to use disable check in automation --- couchpotato/core/providers/automation/itunes/main.py | 3 --- 1 file changed, 3 deletions(-) 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'))] From e9abf982febe141067d91dbfa48e4f308c28dd5d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 22:21:32 +0200 Subject: [PATCH 43/45] Flixter decode json before parsing. closes #2305 --- couchpotato/core/providers/automation/flixster/main.py | 2 +- couchpotato/core/providers/base.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/providers/automation/flixster/main.py b/couchpotato/core/providers/automation/flixster/main.py index 429f9fb7..7fd2f717 100644 --- a/couchpotato/core/providers/automation/flixster/main.py +++ b/couchpotato/core/providers/automation/flixster/main.py @@ -39,7 +39,7 @@ class Flixster(Automation): if not enablers[index]: continue - data = self.getJsonData(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/base.py b/couchpotato/core/providers/base.py index 2fa8c11d..60420a13 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -1,5 +1,5 @@ -import logging 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 @@ -8,6 +8,7 @@ from couchpotato.environment import Env from urlparse import urlparse import cookielib import json +import logging import re import time import traceback @@ -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())) From 66967f8326fa23b09726f1c712b0e330589faba8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 13 Oct 2013 22:37:15 +0200 Subject: [PATCH 44/45] Whatever! #2283 @clinton ;) --- couchpotato/core/plugins/renamer/__init__.py | 1 + 1 file changed, 1 insertion(+) 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', }, } From 5bf3b929a29d78d0a41650d124d8e98fbc7b7a48 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 14 Oct 2013 00:01:38 +0200 Subject: [PATCH 45/45] Detect Windows 8 tablets as touchdevice also. --- couchpotato/static/scripts/couchpotato.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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');