diff --git a/couchpotato/core/media/movie/charts/__init__.py b/couchpotato/core/media/movie/charts/__init__.py new file mode 100644 index 0000000..9c67375 --- /dev/null +++ b/couchpotato/core/media/movie/charts/__init__.py @@ -0,0 +1,34 @@ +from .main import Charts + + +def autoload(): + return Charts() + + +config = [{ + 'name': 'charts', + 'groups': [ + { + 'label': 'Charts', + 'description': 'Displays selected charts on the home page', + 'type': 'list', + 'name': 'charts_providers', + 'tab': 'display', + 'options': [ + { + 'name': 'max_items', + 'default': 5, + 'type': 'int', + 'description': 'Maximum number of items displayed from each chart.', + }, + { + 'name': 'update_interval', + 'default': 12, + 'type': 'int', + 'advanced': True, + 'description': '(hours)', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/charts/main.py b/couchpotato/core/media/movie/charts/main.py new file mode 100644 index 0000000..e6c4b04 --- /dev/null +++ b/couchpotato/core/media/movie/charts/main.py @@ -0,0 +1,60 @@ +import time + +from couchpotato import tryInt +from couchpotato.core.logger import CPLog +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent,fireEvent +from couchpotato.core.plugins.base import Plugin + + +log = CPLog(__name__) + + +class Charts(Plugin): + + update_in_progress = False + + def __init__(self): + addApiView('charts.view', self.automationView) + addEvent('app.load', self.setCrons) + + def setCrons(self): + fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.conf('update_interval', default = 12)) + self.updateViewCache() + + + def automationView(self, force_update = False, **kwargs): + + if force_update: + charts = self.updateViewCache() + else: + charts = self.getCache('charts_cached') + if not charts: + charts = self.updateViewCache() + + return { + 'success': True, + 'count': len(charts), + 'charts': charts + } + + + def updateViewCache(self): + + if self.update_in_progress: + while self.update_in_progress: + time.sleep(1) + catched_charts = self.getCache('charts_cached') + if catched_charts: + return catched_charts + + try: + self.update_in_progress = True + charts = fireEvent('automation.get_chart_list', merge = True) + self.setCache('charts_cached', charts, timeout = 7200 * tryInt(self.conf('update_interval', default = 12))) + except: + log.error('Failed refreshing charts') + + self.update_in_progress = False + + return charts diff --git a/couchpotato/core/media/movie/charts/static/charts.css b/couchpotato/core/media/movie/charts/static/charts.css new file mode 100644 index 0000000..da962a4 --- /dev/null +++ b/couchpotato/core/media/movie/charts/static/charts.css @@ -0,0 +1,240 @@ +.charts { + clear: both; +} + +.charts > h2 { + height: 40px; +} + +.charts .chart { + display: inline-block; + width: 50%; + vertical-align: top; +} + +.charts div.refresh { + margin-top: 10px; + clear:both; + text-align:center; +} + +.charts p.no_charts_enabled { + padding: 0.7em 1em; +} + +.charts div.refresh a { + display:block; +} + +.charts .chart h3 a { + color: #fff; +} + + +.charts .chart .media_result { + display: inline-block; + width: 100%; + height: 150px; +} + +@media all and (max-width: 960px) { + .charts .chart { + width: 50%; + } +} + +@media all and (max-width: 600px) { + .charts .chart { + width: 100%; + } +} + +.charts .chart .media_result .data { + left: 150px; + background: #4e5969; + border: none; +} + + .charts .chart .media_result .data .info { + top: 10px; + left: 15px; + right: 15px; + bottom: 10px; + overflow: hidden; + } + + .charts .chart .media_result .data .info h2 { + white-space: normal; + max-height: 120px; + font-size: 18px; + line-height: 18px; + } + + .charts .chart .media_result .data .info .rating, + .charts .chart .media_result .data .info .genres, + .charts .chart .media_result .data .info .year { + position: static; + display: block; + padding: 0; + opacity: .6; + } + + .charts .chart .media_result .data .info .year { + margin: 10px 0 0; + } + + .charts .chart .media_result .data .info .rating { + font-size: 20px; + float: right; + margin-top: -20px; + } + .charts .chart .media_result .data .info .rating:before { + content: "\e031"; + font-family: 'Elusive-Icons'; + font-size: 14px; + margin: 0 5px 0 0; + vertical-align: bottom; + } + + .charts .chart .media_result .data .info .genres { + font-size: 11px; + font-style: italic; + text-align: right; + } + + .charts .chart .media_result .data .info .plot { + display: block; + font-size: 11px; + overflow: hidden; + text-align: justify; + height: 100%; + z-index: 2; + top: 64px; + position: absolute; + background: #4e5969; + cursor: pointer; + transition: all .4s ease-in-out; + padding: 0 3px 10px 0; + } + .charts .chart .media_result .data:before { + bottom: 0; + content: ''; + display: block; + height: 10px; + right: 0; + left: 0; + bottom: 10px; + position: absolute; + background: linear-gradient( + 0deg, + rgba(78, 89, 105, 1) 0%, + rgba(78, 89, 105, 0) 100% + ); + z-index: 3; + pointer-events: none; + } + + .charts .chart .media_result .data .info .plot.full { + top: 0; + overflow: auto; + } + +.charts .chart .media_result .data { + cursor: default; +} + +.charts .chart .media_result .options { + left: 150px; +} + .charts .chart .media_result .options select[name=title] { width: 100%; } + .charts .chart .media_result .options select[name=profile] { width: 100%; } + .charts .chart .media_result .options select[name=category] { width: 100%; } + + .charts .chart .media_result .button { + position: absolute; + margin: 2px 0 0 0; + right: 15px; + bottom: 15px; + } + + +.charts .chart .media_result .thumbnail { + width: 100px; + position: absolute; + left: 50px; +} + +.charts .chart .media_result div.chart_number { + color: white; + position: absolute; + top: 0; + padding: 10px; + font: bold 2em/1em Helvetica, Sans-Serif; + width: 50px; + height: 100%; +} + +.charts .chart .media_result div.chart_number.chart_in_wanted { + background: rgb(0, 255, 40); /* fallback color */ + background: rgba(0, 255, 40, 0.3); +} +.charts .chart .media_result div.chart_number.chart_in_library { + background: rgb(0, 202, 32); /* fallback color */ + background: rgba(0, 202, 32, 0.3); +} + + +.charts .chart .media_result .actions { + position: absolute; + top: 10px; + right: 10px; + display: none; + width: 90px; +} + .charts .chart .media_result:hover .actions { + display: block; + } + .charts .chart .media_result:hover h2 .title { + opacity: 0; + } + .charts .chart .media_result .data.open .actions { + display: none; + } + + .charts .chart .media_result .actions a { + margin-left: 10px; + vertical-align: middle; + } + + +.toggle_menu { + height: 50px; +} + +.toggle_menu a { + display: block; + width: 50%; + float: left; + color: rgba(255,255,255,.6); + border-bottom: 1px solid rgba(255, 255, 255, 0.0666667); +} + +.toggle_menu a:hover { + border-color: #047792; + border-width: 4px; + color: #fff; +} + +.toggle_menu a.active { + border-bottom: 4px solid #04bce6; + color: #fff; +} + +.toggle_menu a:last-child { + float: right; +} + +.toggle_menu h2 { + height: 40px; +} + diff --git a/couchpotato/core/media/movie/charts/static/charts.js b/couchpotato/core/media/movie/charts/static/charts.js new file mode 100644 index 0000000..5b45369 --- /dev/null +++ b/couchpotato/core/media/movie/charts/static/charts.js @@ -0,0 +1,166 @@ +var Charts = new Class({ + + Implements: [Options, Events], + + initialize: function(options){ + var self = this; + self.setOptions(options); + + self.create(); + }, + + create: function(){ + var self = this; + + self.el_refreshing_text = new Element('span.refreshing', { + 'text': 'Refreshing charts...' + }); + + self.el_refresh_link = new Element('a.refresh', { + 'href': '#', + 'text': 'Refresh charts', + 'events': { + 'click': function(e) { + e.preventDefault(); + self.el.getChildren('div.chart').destroy(); + self.el_refreshing_text.show(); + self.el_refresh_link.hide(); + self.api_request = Api.request('charts.view', { + 'data': { 'force_update': 1 }, + 'onComplete': self.fill.bind(self) + }); + } + } + }).hide(); + + self.el_refresh_container = new Element('div.refresh').grab( + self.el_refreshing_text + ).grab(self.el_refresh_link); + + self.el_no_charts_enabled = new Element('p.no_charts_enabled', { + 'html': 'Hey, it looks like you have no charts enabled at the moment. If you\'d like some great movie suggestions you can go to settings and turn on some charts of your choice.' + }).hide(); + + self.el = new Element('div.charts').grab( + self.el_no_charts_enabled + ).grab(self.el_refresh_container); + + if( Cookie.read('suggestions_charts_menu_selected') === 'charts') + self.el.show(); + else + self.el.hide(); + + self.api_request = Api.request('charts.view', { + 'onComplete': self.fill.bind(self) + }); + + }, + + fill: function(json){ + + var self = this; + + self.el_refreshing_text.hide(); + self.el_refresh_link.show(); + + if(!json || json.count == 0){ + self.el_no_charts_enabled.show(); + self.el_refresh_link.show(); + self.el_refreshing_text.hide(); + } + else { + self.el_no_charts_enabled.hide(); + + json.charts.sort(function(a, b) { + return a.order - b.order; + }); + + Object.each(json.charts, function(chart){ + + var c = new Element('div.chart').grab( + new Element('h3').grab( new Element('a', { + 'text': chart.name, + 'href': chart.url + })) + ); + + var it = 1; + + Object.each(chart.list, function(movie){ + + var m = new Block.Search.MovieItem(movie, { + 'onAdded': function(){ + self.afterAdded(m, movie) + } + }); + var in_database_class = movie.in_wanted ? '.chart_in_wanted' : (movie.in_library ? '.chart_in_library' : ''); + var in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : ''); + m.el.grab( new Element('div.chart_number' + in_database_class, { 'text': it++, 'title': in_database_title })); + m.data_container.grab( + new Element('div.actions').adopt( + new Element('a.add.icon2', { + 'title': 'Add movie with your default quality', + 'data-add': movie.imdb, + 'events': { + 'click': m.showOptions.bind(m) + } + }), + $(new MA.IMDB(m)), + $(new MA.Trailer(m, { + 'height': 150 + })) + ) + ); + m.data_container.removeEvents('click'); + + var plot = false; + if(m.info.plot && m.info.plot.length > 0) + plot = m.info.plot; + + // Add rating + m.info_container.adopt( + m.rating = m.info.rating && m.info.rating.imdb && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', { + 'text': parseFloat(m.info.rating.imdb[0]), + 'title': parseInt(m.info.rating.imdb[1]) + ' votes' + }) : null, + m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', { + 'text': m.info.genres.slice(0, 3).join(', ') + }) : null, + m.plot = plot ? new Element('span.plot', { + 'text': plot, + 'events': { + 'click': function(){ + this.toggleClass('full') + } + } + }) : null + ) + + $(m).inject(c); + + }); + + $(c).inject(self.el_refresh_container, 'before'); + + }); + + } + + self.fireEvent('loaded'); + + }, + + afterAdded: function(m, movie){ + var self = this; + + $(m).getElement('div.chart_number') + .addClass('chart_in_wanted') + .set('title', 'Movie in wanted list'); + + }, + + toElement: function(){ + return this.el; + } + +}) diff --git a/couchpotato/core/media/movie/providers/automation/base.py b/couchpotato/core/media/movie/providers/automation/base.py index 5b22865..1a8d981 100644 --- a/couchpotato/core/media/movie/providers/automation/base.py +++ b/couchpotato/core/media/movie/providers/automation/base.py @@ -13,6 +13,7 @@ log = CPLog(__name__) class Automation(AutomationBase): enabled_option = 'automation_enabled' + chart_enabled_option = 'chart_display_enabled' http_time_between_calls = 2 interval = 1800 @@ -20,6 +21,7 @@ class Automation(AutomationBase): def __init__(self): addEvent('automation.get_movies', self._getMovies) + addEvent('automation.get_chart_list', self._getChartList) def _getMovies(self): @@ -34,6 +36,13 @@ class Automation(AutomationBase): return self.getIMDBids() + def _getChartList(self): + + if not (self.conf(self.chart_enabled_option) or self.conf(self.chart_enabled_option) is None): + return + + return self.getChartList() + def search(self, name, year = None, imdb_only = False): prop_name = 'automation.cached.%s.%s' % (name, year) @@ -94,5 +103,9 @@ class Automation(AutomationBase): def getIMDBids(self): return [] + def getChartList(self): + # Example return: [ {'name': 'Display name of list', 'url': 'http://example.com/', 'order': 1, 'list': []} ] + return + def canCheck(self): return time.time() > self.last_checked + self.interval diff --git a/couchpotato/core/media/movie/providers/automation/bluray.py b/couchpotato/core/media/movie/providers/automation/bluray.py index b0f99f6..55114fd 100644 --- a/couchpotato/core/media/movie/providers/automation/bluray.py +++ b/couchpotato/core/media/movie/providers/automation/bluray.py @@ -14,6 +14,8 @@ class Bluray(Automation, RSS): interval = 1800 rss_url = 'http://www.blu-ray.com/rss/newreleasesfeed.xml' backlog_url = 'http://www.blu-ray.com/movies/movies.php?show=newreleases&page=%s' + display_url = 'http://www.blu-ray.com/movies/movies.php?show=newreleases' + chart_order = 1 def getIMDBids(self): @@ -77,6 +79,32 @@ class Bluray(Automation, RSS): return movies + def getChartList(self): + # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) + movie_list = {'name': 'Blu-ray.com - New Releases', 'url': self.display_url, 'order': self.chart_order, 'list': []} + max_items = int(self.conf('max_items', section='charts', default=5)) + rss_movies = self.getRSSData(self.rss_url) + + for movie in rss_movies: + name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip() + year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip() + + if not name.find('/') == -1: # make sure it is not a double movie release + continue + + movie = self.search(name, year) + + if movie: + movie_list['list'].append( movie ) + if len(movie_list['list']) >= max_items: + break + + if not movie_list['list']: + return + + return [ movie_list ] + + config = [{ 'name': 'bluray', 'groups': [ @@ -101,5 +129,19 @@ config = [{ }, ], }, + { + 'tab': 'display', + 'list': 'charts_providers', + 'name': 'bluray_charts_display', + 'label': 'Blu-ray.com', + 'description': 'Display new releases from Blu-ray.com', + 'options': [ + { + 'name': 'chart_display_enabled', + 'default': True, + 'type': 'enabler', + }, + ], + }, ], }] diff --git a/couchpotato/core/media/movie/providers/automation/imdb.py b/couchpotato/core/media/movie/providers/automation/imdb.py index 10150f2..0f2e294 100644 --- a/couchpotato/core/media/movie/providers/automation/imdb.py +++ b/couchpotato/core/media/movie/providers/automation/imdb.py @@ -105,6 +105,16 @@ class IMDBAutomation(IMDBBase): 'top250': 'http://www.imdb.com/chart/top', 'boxoffice': 'http://www.imdb.com/chart/', } + chart_names = { + 'theater': 'IMDB - Movies in Theaters', + 'top250': 'IMDB - Top 250 Movies', + 'boxoffice': 'IMDB - Box Office', + } + chart_order = { + 'theater': 2, + 'top250': 4, + 'boxoffice': 3, + } first_table = ['boxoffice'] @@ -144,6 +154,46 @@ class IMDBAutomation(IMDBBase): return movies + def getChartList(self): + # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) + movie_lists = [] + max_items = int(self.conf('max_items', section='charts', default=5)) + + for url in self.chart_urls: + if self.conf('chart_display_%s' % url): + movie_list = {'name': self.chart_names[url], 'url': self.chart_urls[url], 'order': self.chart_order[url], 'list': []} + data = self.getHTMLData(self.chart_urls[url]) + if data: + html = BeautifulSoup(data) + + try: + result_div = html.find('div', attrs = {'id': 'main'}) + + try: + if url in self.first_table: + table = result_div.find('table') + result_div = table if table else result_div + except: + pass + + imdb_ids = getImdb(str(result_div), multiple = True) + + for imdb_id in imdb_ids[0:max_items]: + info = self.getInfo(imdb_id) + movie_list['list'].append(info) + + if self.shuttingDown(): + break + except: + log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) + + if movie_list['list']: + movie_lists.append(movie_list) + + + return movie_lists + + config = [{ 'name': 'imdb', 'groups': [ @@ -206,5 +256,40 @@ config = [{ }, ], }, + { + 'tab': 'display', + 'list': 'charts_providers', + 'name': 'imdb_charts_display', + 'label': 'IMDB', + 'description': 'Display movies from IMDB Charts', + 'options': [ + { + 'name': 'chart_display_enabled', + 'default': True, + 'type': 'enabler', + }, + { + 'name': 'chart_display_theater', + 'type': 'bool', + 'label': 'In Theaters', + 'description': 'New Movies In-Theaters chart', + 'default': False, + }, + { + 'name': 'chart_display_top250', + 'type': 'bool', + 'label': 'TOP 250', + 'description': 'IMDB TOP 250 chart', + 'default': False, + }, + { + 'name': 'chart_display_boxoffice', + 'type': 'bool', + 'label': 'Box office TOP 10', + 'description': 'IMDB Box office TOP 10 chart', + 'default': True, + }, + ], + }, ], }] diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.css b/couchpotato/core/media/movie/suggestion/static/suggest.css index 82f0c21..a719eb2 100644 --- a/couchpotato/core/media/movie/suggestion/static/suggest.css +++ b/couchpotato/core/media/movie/suggestion/static/suggest.css @@ -1,4 +1,6 @@ .suggestions { + clear: both; + padding-top: 10px; } .suggestions > h2 { diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.js b/couchpotato/core/media/movie/suggestion/static/suggest.js index a0547e4..494f045 100644 --- a/couchpotato/core/media/movie/suggestion/static/suggest.js +++ b/couchpotato/core/media/movie/suggestion/static/suggest.js @@ -42,11 +42,10 @@ var SuggestList = new Class({ } } - }).grab( - new Element('h2', { - 'text': 'You might like these' - }) - ); + }); + + var cookie_menu_select = Cookie.read('suggestions_charts_menu_selected'); + if( cookie_menu_select === 'suggestions' || cookie_menu_select === null ) self.el.show(); else self.el.hide(); self.api_request = Api.request('suggestion.view', { 'onComplete': self.fill.bind(self) diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index 41ad541..4075826 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -21,7 +21,9 @@ Page.Home = new Class({ self.chain.chain( self.createAvailable.bind(self), self.createSoon.bind(self), + self.createSuggestionsChartsMenu.bind(self), self.createSuggestions.bind(self), + self.createCharts.bind(self), self.createLate.bind(self) ); @@ -151,7 +153,78 @@ Page.Home = new Class({ $(self.suggestion_list).inject(self.el); + }, + + createCharts: function(){ + var self = this; + + // Charts + self.charts = new Charts({ + 'onLoaded': function(){ + self.chain.callChain(); + } + }); + + $(self.charts).inject(self.el); + + }, + + createSuggestionsChartsMenu: function(){ + var self = this; + + self.el_toggle_menu_suggestions = new Element('a.toggle_suggestions.active', { + 'href': '#', + 'events': { 'click': function(e) { + e.preventDefault(); + self.toggleSuggestionsCharts('suggestions'); + } + } + }).grab( new Element('h2', {'text': 'Suggestions'})); + + self.el_toggle_menu_charts = new Element('a.toggle_charts', { + 'href': '#', + 'events': { 'click': function(e) { + e.preventDefault(); + self.toggleSuggestionsCharts('charts'); + } + } + }).grab( new Element('h2', {'text': 'Charts'})); + + self.el_toggle_menu = new Element('div.toggle_menu').grab( + self.el_toggle_menu_suggestions + ).grab( + self.el_toggle_menu_charts + ); + + var menu_selected = Cookie.read('suggestions_charts_menu_selected'); + if( menu_selected === null ) menu_selected = 'suggestions'; + self.toggleSuggestionsCharts( menu_selected ); + + self.el_toggle_menu.inject(self.el); + + self.chain.callChain(); + + }, + toggleSuggestionsCharts: function(menu_id){ + var self = this; + + switch(menu_id) { + case 'suggestions': + if($(self.suggestion_list)) $(self.suggestion_list).show(); + self.el_toggle_menu_suggestions.addClass('active'); + if($(self.charts)) $(self.charts).hide(); + self.el_toggle_menu_charts.removeClass('active'); + break; + case 'charts': + if($(self.charts)) $(self.charts).show(); + self.el_toggle_menu_charts.addClass('active'); + if($(self.suggestion_list)) $(self.suggestion_list).hide(); + self.el_toggle_menu_suggestions.removeClass('active'); + break; + } + + Cookie.write('suggestions_charts_menu_selected', menu_id, {'duration': 365}); }, createLate: function(){