From 06eb0fd3bebed6087be83f2b3e06a912220ba816 Mon Sep 17 00:00:00 2001 From: P0psicles Date: Sat, 10 Nov 2018 11:35:23 +0100 Subject: [PATCH 01/75] * Cache recommended shows in ShowUpdater * Add link to trakt show (needs the anom from vue) * Add option to disable rec show caching. (default on) --- medusa/__main__.py | 6 +-- medusa/app.py | 3 ++ medusa/show/recommendations/trakt.py | 3 +- medusa/show_updater.py | 44 +++++++++++++++++++ themes-default/slim/static/css/style.css | 5 ++- .../slim/views/addShows_recommended.mako | 9 +++- .../slim/views/addShows_trendingShows.mako | 6 +++ .../slim/views/partials/schedule/list.mako | 2 + themes/dark/assets/css/style.css | 5 ++- themes/dark/assets/js/vendors.js | 2 +- .../dark/templates/addShows_recommended.mako | 9 +++- .../templates/addShows_trendingShows.mako | 6 +++ .../templates/partials/schedule/list.mako | 2 + themes/light/assets/css/style.css | 5 ++- themes/light/assets/js/vendors.js | 2 +- .../light/templates/addShows_recommended.mako | 9 +++- .../templates/addShows_trendingShows.mako | 6 +++ .../templates/partials/schedule/list.mako | 2 + 18 files changed, 108 insertions(+), 18 deletions(-) diff --git a/medusa/__main__.py b/medusa/__main__.py index 807ae93555..0dcc82f6a3 100755 --- a/medusa/__main__.py +++ b/medusa/__main__.py @@ -1172,9 +1172,9 @@ def initialize(self, console_logging=True): app.show_update_scheduler = scheduler.Scheduler(show_updater.ShowUpdater(), cycleTime=datetime.timedelta(hours=1), - threadName='SHOWUPDATER', - start_time=datetime.time(hour=app.SHOWUPDATE_HOUR, - minute=random.randint(0, 59))) + threadName='SHOWUPDATER')#, + # start_time=datetime.time(hour=app.SHOWUPDATE_HOUR, + # minute=random.randint(0, 59))) # snatcher used for manual search, manual picked results app.manual_snatch_scheduler = scheduler.Scheduler(SnatchQueue(), diff --git a/medusa/app.py b/medusa/app.py index 669f7fc069..73840a9c2d 100644 --- a/medusa/app.py +++ b/medusa/app.py @@ -667,6 +667,9 @@ def __init__(self): self.FALLBACK_PLEX_API_URL = 'https://tvdb2.plex.tv' self.TVDB_API_KEY = '0629B785CE550C8D' + # show updater recommeded show caching + self.CACHE_RECOMMEDED_SHOWS = True + app = MedusaApp() for app_key, app_value in app.__dict__.items(): diff --git a/medusa/show/recommendations/trakt.py b/medusa/show/recommendations/trakt.py index 782e065bb3..8b62af2734 100644 --- a/medusa/show/recommendations/trakt.py +++ b/medusa/show/recommendations/trakt.py @@ -50,7 +50,8 @@ def _create_recommended_show(self, series, storage_key=None): """Create the RecommendedShow object from the returned showobj.""" rec_show = RecommendedShow( self, - series['show']['ids'], series['show']['title'], + series['show']['ids']['trakt'], + series['show']['title'], INDEXER_TVDBV2, # indexer series['show']['ids']['tvdb'], **{'rating': series['show']['rating'], diff --git a/medusa/show_updater.py b/medusa/show_updater.py index 393de9dd03..9e01601d30 100644 --- a/medusa/show_updater.py +++ b/medusa/show_updater.py @@ -22,6 +22,7 @@ import threading import time from builtins import object +import datetime from medusa import app, db, network_timezones, ui from medusa.helper.exceptions import CantRefreshShowException, CantUpdateShowException @@ -29,6 +30,10 @@ from medusa.indexers.indexer_exceptions import IndexerException, IndexerUnavailable from medusa.scene_exceptions import refresh_exceptions_cache from medusa.session.core import MedusaSession +from medusa.show.recommendations.anidb import AnidbPopular +from medusa.show.recommendations.imdb import ImdbPopular +from medusa.show.recommendations.trakt import TraktPopular +from simpleanidb import REQUEST_HOT from requests.exceptions import HTTPError, RequestException @@ -226,6 +231,45 @@ def run(self, force=False): else: logger.info(u'Completed but there was nothing to update') + # Update recommended shows from trakt, imdb and anidb + # recommended shows are dogpilled into cache/recommended.dbm + + if app.CACHE_RECOMMEDED_SHOWS: + logger.info(u'Started caching recommended shows') + + # Cache trakt shows + for page_url in ( + 'shows/trending', + 'shows/popular', + 'shows/anticipated', + 'shows/collected', + 'shows/watched', + 'shows/played', + 'recommendations/shows', + 'calendars/all/shows/new/%s/30' % datetime.date.today().strftime('%Y-%m-%d'), + 'calendars/all/shows/premieres/%s/30' % datetime.date.today().strftime('%Y-%m-%d') + ): + try: + TraktPopular().fetch_popular_shows(page_url=page_url) + except Exception as error: + logger.info(u'Could not get trakt recommended shows for %s because of error: %s', page_url, error) + logger.debug(u'Not bothering getting the other trakt lists') + break + + # Cache imdb shows + try: + ImdbPopular().fetch_popular_shows() + except (RequestException, Exception) as error: + logger.info(u'Could not get imdb recommended shows because of error: %s', error) + + # Cache anidb shows + try: + AnidbPopular().fetch_popular_shows(REQUEST_HOT) + except Exception as error: + logger.info(u'Could not get anidb recommended shows because of error: %s', error) + + logger.info(u'Finished caching recommended shows') + self.amActive = False def __del__(self): diff --git a/themes-default/slim/static/css/style.css b/themes-default/slim/static/css/style.css index 655b3b7654..fe23dbf69a 100644 --- a/themes-default/slim/static/css/style.css +++ b/themes-default/slim/static/css/style.css @@ -1082,13 +1082,14 @@ div.recommended-image { border-bottom: 1px solid rgb(17, 17, 17); } -.recommended-container .anidb-url { +.recommended-container .recommended-show-url { float: right; padding-right: 4px; } -.anidb-inline { +.recommeded-show-link-inline { height: 16px; + padding-bottom: 1px; } .recommended-container .default-poster { diff --git a/themes-default/slim/views/addShows_recommended.mako b/themes-default/slim/views/addShows_recommended.mako index 5027aa84af..9591a48507 100644 --- a/themes-default/slim/views/addShows_recommended.mako +++ b/themes-default/slim/views/addShows_recommended.mako @@ -131,10 +131,15 @@ window.app = new Vue({

${int(float(cur_rating)*10)}% % if cur_show.is_anime and cur_show.ids.get('aid'): - - + + % endif + % if cur_show.recommender == 'Trakt Popular': + + + + % endif

${cur_votes} votes diff --git a/themes-default/slim/views/addShows_trendingShows.mako b/themes-default/slim/views/addShows_trendingShows.mako index f5450013d2..2fbcf9d8ad 100644 --- a/themes-default/slim/views/addShows_trendingShows.mako +++ b/themes-default/slim/views/addShows_trendingShows.mako @@ -16,6 +16,12 @@ window.app = new Vue({ return { rootDirs: [] }; + }, + mounted() { + debugger; + $('.recommended-show-url').each(obj, index => { + debugger; + }) } }); diff --git a/themes-default/slim/views/partials/schedule/list.mako b/themes-default/slim/views/partials/schedule/list.mako index a7163e2755..c98c166c1b 100644 --- a/themes-default/slim/views/partials/schedule/list.mako +++ b/themes-default/slim/views/partials/schedule/list.mako @@ -41,6 +41,8 @@ show_div = 'listing-current' else: show_div = 'listing-default' + else: + cur_ep_enddate = cur_result['localtime'] %> diff --git a/themes/dark/assets/css/style.css b/themes/dark/assets/css/style.css index 655b3b7654..fe23dbf69a 100644 --- a/themes/dark/assets/css/style.css +++ b/themes/dark/assets/css/style.css @@ -1082,13 +1082,14 @@ div.recommended-image { border-bottom: 1px solid rgb(17, 17, 17); } -.recommended-container .anidb-url { +.recommended-container .recommended-show-url { float: right; padding-right: 4px; } -.anidb-inline { +.recommeded-show-link-inline { height: 16px; + padding-bottom: 1px; } .recommended-container .default-poster { diff --git a/themes/dark/assets/js/vendors.js b/themes/dark/assets/js/vendors.js index 82065cbaae..a5e06af2b4 100644 --- a/themes/dark/assets/js/vendors.js +++ b/themes/dark/assets/js/vendors.js @@ -1145,7 +1145,7 @@ eval("/* WEBPACK VAR INJECTION */(function(global) {var scope = typeof global != /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { -eval("var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;var _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) {\n return typeof obj;\n} : function (obj) {\n return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n};\n\n(function (global, factory) {\n ( false ? undefined : _typeof(exports)) === 'object' && typeof module !== 'undefined' ? module.exports = factory() : true ? !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :\n\t\t\t\t__WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)) : undefined;\n})(this, function () {\n 'use strict';\n\n function isComputedLazy(item) {\n return item.hasOwnProperty('lazy') && item.lazy;\n }\n\n function isLazyActive(vm, key) {\n return vm[lazyActivePrefix + key];\n }\n\n var lazyActivePrefix = 'async_computed$lazy_active$',\n lazyDataPrefix = 'async_computed$lazy_data$';\n\n function initLazy(data, key) {\n data[lazyActivePrefix + key] = false;\n data[lazyDataPrefix + key] = null;\n }\n\n function makeLazyComputed(key) {\n return {\n get: function get() {\n this[lazyActivePrefix + key] = true;\n return this[lazyDataPrefix + key];\n },\n set: function set(value) {\n this[lazyDataPrefix + key] = value;\n }\n };\n }\n\n function silentSetLazy(vm, key, value) {\n vm[lazyDataPrefix + key] = value;\n }\n\n function silentGetLazy(vm, key) {\n return vm[lazyDataPrefix + key];\n }\n\n var prefix = '_async_computed$';\n var DidNotUpdate = typeof Symbol === 'function' ? Symbol('did-not-update') : {};\n var AsyncComputed = {\n install: function install(Vue, pluginOptions) {\n pluginOptions = pluginOptions || {};\n Vue.config.optionMergeStrategies.asyncComputed = Vue.config.optionMergeStrategies.computed;\n Vue.mixin({\n beforeCreate: function beforeCreate() {\n var optionData = this.$options.data;\n var asyncComputed = this.$options.asyncComputed || {};\n this.$asyncComputed = {};\n if (!Object.keys(asyncComputed).length) return;\n if (!this.$options.computed) this.$options.computed = {};\n\n for (var key in asyncComputed) {\n var getter = getterFn(key, this.$options.asyncComputed[key]);\n this.$options.computed[prefix + key] = getter;\n }\n\n this.$options.data = function vueAsyncComputedInjectedDataFn() {\n var data = (typeof optionData === 'function' ? optionData.call(this) : optionData) || {};\n\n for (var _key in asyncComputed) {\n var item = this.$options.asyncComputed[_key];\n\n if (isComputedLazy(item)) {\n initLazy(data, _key);\n this.$options.computed[_key] = makeLazyComputed(_key);\n } else {\n data[_key] = null;\n }\n }\n\n return data;\n };\n },\n created: function created() {\n var _this = this;\n\n for (var key in this.$options.asyncComputed || {}) {\n var item = this.$options.asyncComputed[key],\n value = generateDefault.call(this, item, pluginOptions);\n\n if (isComputedLazy(item)) {\n silentSetLazy(this, key, value);\n } else {\n this[key] = value;\n }\n }\n\n var _loop = function _loop(_key2) {\n var promiseId = 0;\n\n var watcher = function watcher(newPromise) {\n var thisPromise = ++promiseId;\n\n if (newPromise === DidNotUpdate) {\n return;\n }\n\n if (!newPromise || !newPromise.then) {\n newPromise = Promise.resolve(newPromise);\n }\n\n setAsyncState(_this.$asyncComputed[_key2], 'updating');\n newPromise.then(function (value) {\n if (thisPromise !== promiseId) return;\n setAsyncState(_this.$asyncComputed[_key2], 'success');\n _this[_key2] = value;\n }).catch(function (err) {\n if (thisPromise !== promiseId) return;\n setAsyncState(_this.$asyncComputed[_key2], 'error');\n _this.$asyncComputed[_key2].exception = err;\n if (pluginOptions.errorHandler === false) return;\n var handler = pluginOptions.errorHandler === undefined ? console.error.bind(console, 'Error evaluating async computed property:') : pluginOptions.errorHandler;\n\n if (pluginOptions.useRawError) {\n handler(err);\n } else {\n handler(err.stack);\n }\n });\n };\n\n _this.$asyncComputed[_key2] = {\n exception: null,\n update: function update() {\n watcher(getterOnly(_this.$options.asyncComputed[_key2])());\n }\n };\n setAsyncState(_this.$asyncComputed[_key2], 'updating');\n\n _this.$watch(prefix + _key2, watcher, {\n immediate: true\n });\n };\n\n for (var _key2 in this.$options.asyncComputed || {}) {\n _loop(_key2);\n }\n }\n });\n }\n };\n\n function setAsyncState(stateObject, state) {\n stateObject.state = state;\n stateObject.updating = state === 'updating';\n stateObject.error = state === 'error';\n stateObject.success = state === 'success';\n }\n\n function getterOnly(fn) {\n if (typeof fn === 'function') return fn;\n return fn.get;\n }\n\n function getterFn(key, fn) {\n if (typeof fn === 'function') return fn;\n var getter = fn.get;\n\n if (fn.hasOwnProperty('watch')) {\n var previousGetter = getter;\n\n getter = function getter() {\n fn.watch.call(this);\n return previousGetter.call(this);\n };\n }\n\n if (fn.hasOwnProperty('shouldUpdate')) {\n var _previousGetter = getter;\n\n getter = function getter() {\n if (fn.shouldUpdate.call(this)) {\n return _previousGetter.call(this);\n }\n\n return DidNotUpdate;\n };\n }\n\n if (isComputedLazy(fn)) {\n var nonLazy = getter;\n\n getter = function lazyGetter() {\n if (isLazyActive(this, key)) {\n return nonLazy.call(this);\n } else {\n return silentGetLazy(this, key);\n }\n };\n }\n\n return getter;\n }\n\n function generateDefault(fn, pluginOptions) {\n var defaultValue = null;\n\n if ('default' in fn) {\n defaultValue = fn.default;\n } else if ('default' in pluginOptions) {\n defaultValue = pluginOptions.default;\n }\n\n if (typeof defaultValue === 'function') {\n return defaultValue.call(this);\n } else {\n return defaultValue;\n }\n }\n /* istanbul ignore if */\n\n\n if (typeof window !== 'undefined' && window.Vue) {\n // Auto install in dist mode\n window.Vue.use(AsyncComputed);\n }\n\n return AsyncComputed;\n});\n\n//# sourceURL=webpack:///./node_modules/vue-async-computed/dist/vue-async-computed.js?"); +eval("var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;var _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) {\n return typeof obj;\n} : function (obj) {\n return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n};\n\n(function (global, factory) {\n ( false ? undefined : _typeof(exports)) === 'object' && typeof module !== 'undefined' ? module.exports = factory() : true ? !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :\n\t\t\t\t__WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)) : undefined;\n})(this, function () {\n 'use strict';\n\n function isComputedLazy(item) {\n return item.hasOwnProperty('lazy') && item.lazy;\n }\n\n function isLazyActive(vm, key) {\n return vm[lazyActivePrefix + key];\n }\n\n var lazyActivePrefix = 'async_computed$lazy_active$',\n lazyDataPrefix = 'async_computed$lazy_data$';\n\n function initLazy(data, key) {\n data[lazyActivePrefix + key] = false;\n data[lazyDataPrefix + key] = null;\n }\n\n function makeLazyComputed(key) {\n return {\n get: function get() {\n this[lazyActivePrefix + key] = true;\n return this[lazyDataPrefix + key];\n },\n set: function set(value) {\n this[lazyDataPrefix + key] = value;\n }\n };\n }\n\n function silentSetLazy(vm, key, value) {\n vm[lazyDataPrefix + key] = value;\n }\n\n function silentGetLazy(vm, key) {\n return vm[lazyDataPrefix + key];\n }\n\n var prefix = '_async_computed$';\n var DidNotUpdate = typeof Symbol === 'function' ? Symbol('did-not-update') : {};\n var AsyncComputed = {\n install: function install(Vue, pluginOptions) {\n pluginOptions = pluginOptions || {};\n Vue.config.optionMergeStrategies.asyncComputed = Vue.config.optionMergeStrategies.computed;\n Vue.mixin({\n beforeCreate: function beforeCreate() {\n var optionData = this.$options.data;\n if (!this.$options.computed) this.$options.computed = {};\n\n for (var key in this.$options.asyncComputed || {}) {\n this.$options.computed[prefix + key] = getterFn(key, this.$options.asyncComputed[key]);\n }\n\n this.$options.data = function vueAsyncComputedInjectedDataFn() {\n var data = (typeof optionData === 'function' ? optionData.call(this) : optionData) || {};\n\n for (var _key in this.$options.asyncComputed || {}) {\n var item = this.$options.asyncComputed[_key];\n\n if (isComputedLazy(item)) {\n initLazy(data, _key);\n this.$options.computed[_key] = makeLazyComputed(_key);\n } else {\n data[_key] = null;\n }\n }\n\n return data;\n };\n },\n created: function created() {\n var _this = this;\n\n for (var key in this.$options.asyncComputed || {}) {\n var item = this.$options.asyncComputed[key],\n value = generateDefault.call(this, item, pluginOptions);\n\n if (isComputedLazy(item)) {\n silentSetLazy(this, key, value);\n } else {\n this[key] = value;\n }\n }\n\n var _loop = function _loop(_key2) {\n var promiseId = 0;\n\n _this.$watch(prefix + _key2, function (newPromise) {\n var thisPromise = ++promiseId;\n\n if (newPromise === DidNotUpdate) {\n return;\n }\n\n if (!newPromise || !newPromise.then) {\n newPromise = Promise.resolve(newPromise);\n }\n\n newPromise.then(function (value) {\n if (thisPromise !== promiseId) return;\n _this[_key2] = value;\n }).catch(function (err) {\n if (thisPromise !== promiseId) return;\n if (pluginOptions.errorHandler === false) return;\n var handler = pluginOptions.errorHandler === undefined ? console.error.bind(console, 'Error evaluating async computed property:') : pluginOptions.errorHandler;\n\n if (pluginOptions.useRawError) {\n handler(err);\n } else {\n handler(err.stack);\n }\n });\n }, {\n immediate: true\n });\n };\n\n for (var _key2 in this.$options.asyncComputed || {}) {\n _loop(_key2);\n }\n }\n });\n }\n };\n\n function getterFn(key, fn) {\n if (typeof fn === 'function') return fn;\n var getter = fn.get;\n\n if (fn.hasOwnProperty('watch')) {\n var previousGetter = getter;\n\n getter = function getter() {\n fn.watch.call(this);\n return previousGetter.call(this);\n };\n }\n\n if (fn.hasOwnProperty('shouldUpdate')) {\n var _previousGetter = getter;\n\n getter = function getter() {\n if (fn.shouldUpdate.call(this)) {\n return _previousGetter.call(this);\n }\n\n return DidNotUpdate;\n };\n }\n\n if (isComputedLazy(fn)) {\n var nonLazy = getter;\n\n getter = function lazyGetter() {\n if (isLazyActive(this, key)) {\n return nonLazy.call(this);\n } else {\n return silentGetLazy(this, key);\n }\n };\n }\n\n return getter;\n }\n\n function generateDefault(fn, pluginOptions) {\n var defaultValue = null;\n\n if ('default' in fn) {\n defaultValue = fn.default;\n } else if ('default' in pluginOptions) {\n defaultValue = pluginOptions.default;\n }\n\n if (typeof defaultValue === 'function') {\n return defaultValue.call(this);\n } else {\n return defaultValue;\n }\n }\n /* istanbul ignore if */\n\n\n if (typeof window !== 'undefined' && window.Vue) {\n // Auto install in dist mode\n window.Vue.use(AsyncComputed);\n }\n\n return AsyncComputed;\n});\n\n//# sourceURL=webpack:///./node_modules/vue-async-computed/dist/vue-async-computed.js?"); /***/ }), diff --git a/themes/dark/templates/addShows_recommended.mako b/themes/dark/templates/addShows_recommended.mako index 5027aa84af..9591a48507 100644 --- a/themes/dark/templates/addShows_recommended.mako +++ b/themes/dark/templates/addShows_recommended.mako @@ -131,10 +131,15 @@ window.app = new Vue({

${int(float(cur_rating)*10)}% % if cur_show.is_anime and cur_show.ids.get('aid'): - - + + % endif + % if cur_show.recommender == 'Trakt Popular': + + + + % endif

${cur_votes} votes diff --git a/themes/dark/templates/addShows_trendingShows.mako b/themes/dark/templates/addShows_trendingShows.mako index f5450013d2..2fbcf9d8ad 100644 --- a/themes/dark/templates/addShows_trendingShows.mako +++ b/themes/dark/templates/addShows_trendingShows.mako @@ -16,6 +16,12 @@ window.app = new Vue({ return { rootDirs: [] }; + }, + mounted() { + debugger; + $('.recommended-show-url').each(obj, index => { + debugger; + }) } }); diff --git a/themes/dark/templates/partials/schedule/list.mako b/themes/dark/templates/partials/schedule/list.mako index a7163e2755..c98c166c1b 100644 --- a/themes/dark/templates/partials/schedule/list.mako +++ b/themes/dark/templates/partials/schedule/list.mako @@ -41,6 +41,8 @@ show_div = 'listing-current' else: show_div = 'listing-default' + else: + cur_ep_enddate = cur_result['localtime'] %> diff --git a/themes/light/assets/css/style.css b/themes/light/assets/css/style.css index 655b3b7654..fe23dbf69a 100644 --- a/themes/light/assets/css/style.css +++ b/themes/light/assets/css/style.css @@ -1082,13 +1082,14 @@ div.recommended-image { border-bottom: 1px solid rgb(17, 17, 17); } -.recommended-container .anidb-url { +.recommended-container .recommended-show-url { float: right; padding-right: 4px; } -.anidb-inline { +.recommeded-show-link-inline { height: 16px; + padding-bottom: 1px; } .recommended-container .default-poster { diff --git a/themes/light/assets/js/vendors.js b/themes/light/assets/js/vendors.js index 82065cbaae..a5e06af2b4 100644 --- a/themes/light/assets/js/vendors.js +++ b/themes/light/assets/js/vendors.js @@ -1145,7 +1145,7 @@ eval("/* WEBPACK VAR INJECTION */(function(global) {var scope = typeof global != /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { -eval("var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;var _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) {\n return typeof obj;\n} : function (obj) {\n return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n};\n\n(function (global, factory) {\n ( false ? undefined : _typeof(exports)) === 'object' && typeof module !== 'undefined' ? module.exports = factory() : true ? !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :\n\t\t\t\t__WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)) : undefined;\n})(this, function () {\n 'use strict';\n\n function isComputedLazy(item) {\n return item.hasOwnProperty('lazy') && item.lazy;\n }\n\n function isLazyActive(vm, key) {\n return vm[lazyActivePrefix + key];\n }\n\n var lazyActivePrefix = 'async_computed$lazy_active$',\n lazyDataPrefix = 'async_computed$lazy_data$';\n\n function initLazy(data, key) {\n data[lazyActivePrefix + key] = false;\n data[lazyDataPrefix + key] = null;\n }\n\n function makeLazyComputed(key) {\n return {\n get: function get() {\n this[lazyActivePrefix + key] = true;\n return this[lazyDataPrefix + key];\n },\n set: function set(value) {\n this[lazyDataPrefix + key] = value;\n }\n };\n }\n\n function silentSetLazy(vm, key, value) {\n vm[lazyDataPrefix + key] = value;\n }\n\n function silentGetLazy(vm, key) {\n return vm[lazyDataPrefix + key];\n }\n\n var prefix = '_async_computed$';\n var DidNotUpdate = typeof Symbol === 'function' ? Symbol('did-not-update') : {};\n var AsyncComputed = {\n install: function install(Vue, pluginOptions) {\n pluginOptions = pluginOptions || {};\n Vue.config.optionMergeStrategies.asyncComputed = Vue.config.optionMergeStrategies.computed;\n Vue.mixin({\n beforeCreate: function beforeCreate() {\n var optionData = this.$options.data;\n var asyncComputed = this.$options.asyncComputed || {};\n this.$asyncComputed = {};\n if (!Object.keys(asyncComputed).length) return;\n if (!this.$options.computed) this.$options.computed = {};\n\n for (var key in asyncComputed) {\n var getter = getterFn(key, this.$options.asyncComputed[key]);\n this.$options.computed[prefix + key] = getter;\n }\n\n this.$options.data = function vueAsyncComputedInjectedDataFn() {\n var data = (typeof optionData === 'function' ? optionData.call(this) : optionData) || {};\n\n for (var _key in asyncComputed) {\n var item = this.$options.asyncComputed[_key];\n\n if (isComputedLazy(item)) {\n initLazy(data, _key);\n this.$options.computed[_key] = makeLazyComputed(_key);\n } else {\n data[_key] = null;\n }\n }\n\n return data;\n };\n },\n created: function created() {\n var _this = this;\n\n for (var key in this.$options.asyncComputed || {}) {\n var item = this.$options.asyncComputed[key],\n value = generateDefault.call(this, item, pluginOptions);\n\n if (isComputedLazy(item)) {\n silentSetLazy(this, key, value);\n } else {\n this[key] = value;\n }\n }\n\n var _loop = function _loop(_key2) {\n var promiseId = 0;\n\n var watcher = function watcher(newPromise) {\n var thisPromise = ++promiseId;\n\n if (newPromise === DidNotUpdate) {\n return;\n }\n\n if (!newPromise || !newPromise.then) {\n newPromise = Promise.resolve(newPromise);\n }\n\n setAsyncState(_this.$asyncComputed[_key2], 'updating');\n newPromise.then(function (value) {\n if (thisPromise !== promiseId) return;\n setAsyncState(_this.$asyncComputed[_key2], 'success');\n _this[_key2] = value;\n }).catch(function (err) {\n if (thisPromise !== promiseId) return;\n setAsyncState(_this.$asyncComputed[_key2], 'error');\n _this.$asyncComputed[_key2].exception = err;\n if (pluginOptions.errorHandler === false) return;\n var handler = pluginOptions.errorHandler === undefined ? console.error.bind(console, 'Error evaluating async computed property:') : pluginOptions.errorHandler;\n\n if (pluginOptions.useRawError) {\n handler(err);\n } else {\n handler(err.stack);\n }\n });\n };\n\n _this.$asyncComputed[_key2] = {\n exception: null,\n update: function update() {\n watcher(getterOnly(_this.$options.asyncComputed[_key2])());\n }\n };\n setAsyncState(_this.$asyncComputed[_key2], 'updating');\n\n _this.$watch(prefix + _key2, watcher, {\n immediate: true\n });\n };\n\n for (var _key2 in this.$options.asyncComputed || {}) {\n _loop(_key2);\n }\n }\n });\n }\n };\n\n function setAsyncState(stateObject, state) {\n stateObject.state = state;\n stateObject.updating = state === 'updating';\n stateObject.error = state === 'error';\n stateObject.success = state === 'success';\n }\n\n function getterOnly(fn) {\n if (typeof fn === 'function') return fn;\n return fn.get;\n }\n\n function getterFn(key, fn) {\n if (typeof fn === 'function') return fn;\n var getter = fn.get;\n\n if (fn.hasOwnProperty('watch')) {\n var previousGetter = getter;\n\n getter = function getter() {\n fn.watch.call(this);\n return previousGetter.call(this);\n };\n }\n\n if (fn.hasOwnProperty('shouldUpdate')) {\n var _previousGetter = getter;\n\n getter = function getter() {\n if (fn.shouldUpdate.call(this)) {\n return _previousGetter.call(this);\n }\n\n return DidNotUpdate;\n };\n }\n\n if (isComputedLazy(fn)) {\n var nonLazy = getter;\n\n getter = function lazyGetter() {\n if (isLazyActive(this, key)) {\n return nonLazy.call(this);\n } else {\n return silentGetLazy(this, key);\n }\n };\n }\n\n return getter;\n }\n\n function generateDefault(fn, pluginOptions) {\n var defaultValue = null;\n\n if ('default' in fn) {\n defaultValue = fn.default;\n } else if ('default' in pluginOptions) {\n defaultValue = pluginOptions.default;\n }\n\n if (typeof defaultValue === 'function') {\n return defaultValue.call(this);\n } else {\n return defaultValue;\n }\n }\n /* istanbul ignore if */\n\n\n if (typeof window !== 'undefined' && window.Vue) {\n // Auto install in dist mode\n window.Vue.use(AsyncComputed);\n }\n\n return AsyncComputed;\n});\n\n//# sourceURL=webpack:///./node_modules/vue-async-computed/dist/vue-async-computed.js?"); +eval("var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;var _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) {\n return typeof obj;\n} : function (obj) {\n return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n};\n\n(function (global, factory) {\n ( false ? undefined : _typeof(exports)) === 'object' && typeof module !== 'undefined' ? module.exports = factory() : true ? !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :\n\t\t\t\t__WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)) : undefined;\n})(this, function () {\n 'use strict';\n\n function isComputedLazy(item) {\n return item.hasOwnProperty('lazy') && item.lazy;\n }\n\n function isLazyActive(vm, key) {\n return vm[lazyActivePrefix + key];\n }\n\n var lazyActivePrefix = 'async_computed$lazy_active$',\n lazyDataPrefix = 'async_computed$lazy_data$';\n\n function initLazy(data, key) {\n data[lazyActivePrefix + key] = false;\n data[lazyDataPrefix + key] = null;\n }\n\n function makeLazyComputed(key) {\n return {\n get: function get() {\n this[lazyActivePrefix + key] = true;\n return this[lazyDataPrefix + key];\n },\n set: function set(value) {\n this[lazyDataPrefix + key] = value;\n }\n };\n }\n\n function silentSetLazy(vm, key, value) {\n vm[lazyDataPrefix + key] = value;\n }\n\n function silentGetLazy(vm, key) {\n return vm[lazyDataPrefix + key];\n }\n\n var prefix = '_async_computed$';\n var DidNotUpdate = typeof Symbol === 'function' ? Symbol('did-not-update') : {};\n var AsyncComputed = {\n install: function install(Vue, pluginOptions) {\n pluginOptions = pluginOptions || {};\n Vue.config.optionMergeStrategies.asyncComputed = Vue.config.optionMergeStrategies.computed;\n Vue.mixin({\n beforeCreate: function beforeCreate() {\n var optionData = this.$options.data;\n if (!this.$options.computed) this.$options.computed = {};\n\n for (var key in this.$options.asyncComputed || {}) {\n this.$options.computed[prefix + key] = getterFn(key, this.$options.asyncComputed[key]);\n }\n\n this.$options.data = function vueAsyncComputedInjectedDataFn() {\n var data = (typeof optionData === 'function' ? optionData.call(this) : optionData) || {};\n\n for (var _key in this.$options.asyncComputed || {}) {\n var item = this.$options.asyncComputed[_key];\n\n if (isComputedLazy(item)) {\n initLazy(data, _key);\n this.$options.computed[_key] = makeLazyComputed(_key);\n } else {\n data[_key] = null;\n }\n }\n\n return data;\n };\n },\n created: function created() {\n var _this = this;\n\n for (var key in this.$options.asyncComputed || {}) {\n var item = this.$options.asyncComputed[key],\n value = generateDefault.call(this, item, pluginOptions);\n\n if (isComputedLazy(item)) {\n silentSetLazy(this, key, value);\n } else {\n this[key] = value;\n }\n }\n\n var _loop = function _loop(_key2) {\n var promiseId = 0;\n\n _this.$watch(prefix + _key2, function (newPromise) {\n var thisPromise = ++promiseId;\n\n if (newPromise === DidNotUpdate) {\n return;\n }\n\n if (!newPromise || !newPromise.then) {\n newPromise = Promise.resolve(newPromise);\n }\n\n newPromise.then(function (value) {\n if (thisPromise !== promiseId) return;\n _this[_key2] = value;\n }).catch(function (err) {\n if (thisPromise !== promiseId) return;\n if (pluginOptions.errorHandler === false) return;\n var handler = pluginOptions.errorHandler === undefined ? console.error.bind(console, 'Error evaluating async computed property:') : pluginOptions.errorHandler;\n\n if (pluginOptions.useRawError) {\n handler(err);\n } else {\n handler(err.stack);\n }\n });\n }, {\n immediate: true\n });\n };\n\n for (var _key2 in this.$options.asyncComputed || {}) {\n _loop(_key2);\n }\n }\n });\n }\n };\n\n function getterFn(key, fn) {\n if (typeof fn === 'function') return fn;\n var getter = fn.get;\n\n if (fn.hasOwnProperty('watch')) {\n var previousGetter = getter;\n\n getter = function getter() {\n fn.watch.call(this);\n return previousGetter.call(this);\n };\n }\n\n if (fn.hasOwnProperty('shouldUpdate')) {\n var _previousGetter = getter;\n\n getter = function getter() {\n if (fn.shouldUpdate.call(this)) {\n return _previousGetter.call(this);\n }\n\n return DidNotUpdate;\n };\n }\n\n if (isComputedLazy(fn)) {\n var nonLazy = getter;\n\n getter = function lazyGetter() {\n if (isLazyActive(this, key)) {\n return nonLazy.call(this);\n } else {\n return silentGetLazy(this, key);\n }\n };\n }\n\n return getter;\n }\n\n function generateDefault(fn, pluginOptions) {\n var defaultValue = null;\n\n if ('default' in fn) {\n defaultValue = fn.default;\n } else if ('default' in pluginOptions) {\n defaultValue = pluginOptions.default;\n }\n\n if (typeof defaultValue === 'function') {\n return defaultValue.call(this);\n } else {\n return defaultValue;\n }\n }\n /* istanbul ignore if */\n\n\n if (typeof window !== 'undefined' && window.Vue) {\n // Auto install in dist mode\n window.Vue.use(AsyncComputed);\n }\n\n return AsyncComputed;\n});\n\n//# sourceURL=webpack:///./node_modules/vue-async-computed/dist/vue-async-computed.js?"); /***/ }), diff --git a/themes/light/templates/addShows_recommended.mako b/themes/light/templates/addShows_recommended.mako index 5027aa84af..9591a48507 100644 --- a/themes/light/templates/addShows_recommended.mako +++ b/themes/light/templates/addShows_recommended.mako @@ -131,10 +131,15 @@ window.app = new Vue({

${int(float(cur_rating)*10)}% % if cur_show.is_anime and cur_show.ids.get('aid'): - - + + % endif + % if cur_show.recommender == 'Trakt Popular': + + + + % endif

${cur_votes} votes diff --git a/themes/light/templates/addShows_trendingShows.mako b/themes/light/templates/addShows_trendingShows.mako index f5450013d2..2fbcf9d8ad 100644 --- a/themes/light/templates/addShows_trendingShows.mako +++ b/themes/light/templates/addShows_trendingShows.mako @@ -16,6 +16,12 @@ window.app = new Vue({ return { rootDirs: [] }; + }, + mounted() { + debugger; + $('.recommended-show-url').each(obj, index => { + debugger; + }) } }); diff --git a/themes/light/templates/partials/schedule/list.mako b/themes/light/templates/partials/schedule/list.mako index a7163e2755..c98c166c1b 100644 --- a/themes/light/templates/partials/schedule/list.mako +++ b/themes/light/templates/partials/schedule/list.mako @@ -41,6 +41,8 @@ show_div = 'listing-current' else: show_div = 'listing-default' + else: + cur_ep_enddate = cur_result['localtime'] %> From 4b42002a6483f8155080df2882a548d4de6bb36c Mon Sep 17 00:00:00 2001 From: P0psicles Date: Sun, 25 Nov 2018 12:43:36 +0100 Subject: [PATCH 02/75] Added recommended shows to store. Moved addShows jquery, to vue mounted. First start with working vue recommended show page. --- medusa/app.py | 5 +- medusa/databases/cache_db.py | 24 +++- medusa/server/api/v2/recommended.py | 59 ++++++++ medusa/server/core.py | 5 + medusa/server/web/home/add_shows.py | 11 +- medusa/show/recommendations/anidb.py | 34 +++-- medusa/show/recommendations/imdb.py | 18 +-- medusa/show/recommendations/recommended.py | 136 ++++++++++++++++-- medusa/show/recommendations/trakt.py | 22 +-- medusa/show_updater.py | 66 +++++---- themes-default/slim/src/store/index.js | 2 + .../slim/src/store/modules/index.js | 1 + .../slim/src/store/mutation-types.js | 5 +- themes-default/slim/static/css/style.css | 2 +- .../slim/views/addShows_recommended.mako | 4 +- themes-default/slim/views/layouts/main.mako | 5 - themes/dark/assets/css/style.css | 2 +- themes/dark/assets/js/medusa-runtime.js | 22 ++- .../dark/templates/addShows_recommended.mako | 4 +- themes/dark/templates/layouts/main.mako | 5 - themes/light/assets/css/style.css | 2 +- themes/light/assets/js/medusa-runtime.js | 22 ++- .../light/templates/addShows_recommended.mako | 4 +- themes/light/templates/layouts/main.mako | 5 - 24 files changed, 356 insertions(+), 109 deletions(-) create mode 100644 medusa/server/api/v2/recommended.py diff --git a/medusa/app.py b/medusa/app.py index 73840a9c2d..13a8d3984f 100644 --- a/medusa/app.py +++ b/medusa/app.py @@ -668,7 +668,10 @@ def __init__(self): self.TVDB_API_KEY = '0629B785CE550C8D' # show updater recommeded show caching - self.CACHE_RECOMMEDED_SHOWS = True + self.CACHE_RECOMMENDED_SHOWS = True + self.CACHE_RECOMMENDED_TRAKT = True + self.CACHE_RECOMMENDED_IMDB = True + self.CACHE_RECOMMENDED_ANIDB = True app = MedusaApp() diff --git a/medusa/databases/cache_db.py b/medusa/databases/cache_db.py index 7c8bc1d0f5..4efcf65810 100644 --- a/medusa/databases/cache_db.py +++ b/medusa/databases/cache_db.py @@ -195,6 +195,28 @@ def inc_major_version(self): major_version, minor_version = self.connection.version major_version += 1 self.connection.action('UPDATE db_version SET db_version = ?;', [major_version]) - log.info('[CACHE-DB] Updated major version to: {}.{}', *self.connection.version) + return self.connection.version return self.connection.version + + +class AddRecommendedTable(ClearProviderTables): + """Add table to cache the recommended shows.""" + + def test(self): + return self.hasTable('recommended') + + def execute(self): + self.connection.action( + """CREATE TABLE "recommended" ( + `recommended_id` INTEGER PRIMARY KEY AUTOINCREMENT, + `source` INTEGER NOT NULL, + `series_id` INTEGER NOT NULL, + `default_indexer` INTEGER, + `title` TEXT NOT NULL, + `rating` NUMERIC, + `votes` INTEGER, + `is_anime` INTEGER DEFAULT 0, + `image_href` TEXT + )""" + ) diff --git a/medusa/server/api/v2/recommended.py b/medusa/server/api/v2/recommended.py new file mode 100644 index 0000000000..2b27e6fc7c --- /dev/null +++ b/medusa/server/api/v2/recommended.py @@ -0,0 +1,59 @@ +# coding=utf-8 +"""Request handler for series and episodes.""" +from __future__ import unicode_literals + +import logging + + +from medusa.indexers.indexer_config import EXTERNAL_IMDB, EXTERNAL_ANIDB, EXTERNAL_TRAKT +from medusa.logger.adapters.style import BraceAdapter +from medusa.server.api.v2.base import ( + BaseRequestHandler, + BooleanField, + IntegerField, + ListField, + StringField, + iter_nested_items, + set_nested_value +) +from medusa.show.recommendations.recommended import get_recommended_shows +from medusa.show.recommendations.anidb import AnidbPopular +from medusa.show.recommendations.imdb import ImdbPopular +from medusa.show.recommendations.trakt import TraktPopular +from simpleanidb import REQUEST_HOT +from medusa.tv.series import Series, SeriesIdentifier + +from six import itervalues, viewitems + +from tornado.escape import json_decode + +log = BraceAdapter(logging.getLogger(__name__)) +log.logger.addHandler(logging.NullHandler()) + + +class RecommendedHandler(BaseRequestHandler): + """Series request handler.""" + + #: resource name + name = 'recommended' + + identifier = ('identifier', r'\w+') + #: path param + path_param = ('path_param', r'\w+') + allowed_methods = ('GET',) + + def http_get(self, identifier, path_param=None): + """Query available recommended show lists.""" + + if identifier and identifier not in ('anidb', 'trakt', 'imdb'): + return self._bad_request("Invalid recommended list identifier '{0}'".format(identifier)) + + data = {} + + recommended_mappings = {'imdb': EXTERNAL_IMDB, 'anidb': EXTERNAL_ANIDB, 'trakt': EXTERNAL_TRAKT} + shows = get_recommended_shows(source=recommended_mappings.get(identifier)) + + if shows: + data = [show.to_json() for show in shows] + + return self._ok(data) diff --git a/medusa/server/core.py b/medusa/server/core.py index 96afc8b23f..e1ffe7978c 100644 --- a/medusa/server/core.py +++ b/medusa/server/core.py @@ -25,11 +25,13 @@ from medusa.server.api.v2.episodes import EpisodeHandler from medusa.server.api.v2.internal import InternalHandler from medusa.server.api.v2.log import LogHandler +from medusa.server.api.v2.recommended import RecommendedHandler from medusa.server.api.v2.series import SeriesHandler from medusa.server.api.v2.series_asset import SeriesAssetHandler from medusa.server.api.v2.series_legacy import SeriesLegacyHandler from medusa.server.api.v2.series_operation import SeriesOperationHandler from medusa.server.api.v2.stats import StatsHandler + from medusa.server.web import ( CalendarHandler, KeyHandler, @@ -109,6 +111,9 @@ def get_apiv2_handlers(base): # /api/v2/authenticate AuthHandler.create_app_handler(base), + # /api/v2/recommeded + RecommendedHandler.create_app_handler(base), + # Always keep this last! NotFoundHandler.create_app_handler(base) ] diff --git a/medusa/server/web/home/add_shows.py b/medusa/server/web/home/add_shows.py index 2953908f92..73ad6c6a8f 100644 --- a/medusa/server/web/home/add_shows.py +++ b/medusa/server/web/home/add_shows.py @@ -212,7 +212,7 @@ def popularAnime(self, list_type=REQUEST_HOT): """ Fetches list recommeded shows from anidb.info. """ - t = PageTemplate(rh=self, filename='addShows_recommended.mako') + t = PageTemplate(rh=self, filename='addShows_recommended_vue.mako') e = None try: @@ -226,6 +226,15 @@ def popularAnime(self, list_type=REQUEST_HOT): enable_anime_options=True, blacklist=[], whitelist=[], controller='addShows', action='recommendedShows', realpage='popularAnime') + def recommended(self): + """ + Serve Vue page addShows_recommeded_vue.mako + """ + t = PageTemplate(rh=self, filename='addShows_recommended_vue.mako') + + return t.render(title='Recommended shows', header='Recommended shows', + controller='addShows', action='recommendedShows', realpage='recommended') + def addShowToBlacklist(self, seriesid): # URL parameters data = {'shows': [{'ids': {'tvdb': seriesid}}]} diff --git a/medusa/show/recommendations/anidb.py b/medusa/show/recommendations/anidb.py index 84fff490a2..73c532d3f6 100644 --- a/medusa/show/recommendations/anidb.py +++ b/medusa/show/recommendations/anidb.py @@ -9,11 +9,11 @@ from medusa import app from medusa.cache import recommended_series_cache -from medusa.indexers.indexer_config import INDEXER_TVDBV2 +from medusa.indexers.indexer_config import INDEXER_TVDBV2, EXTERNAL_ANIDB from medusa.logger.adapters.style import BraceAdapter from medusa.session.core import MedusaSession from medusa.show.recommendations.recommended import ( - MissingTvdbMapping, RecommendedShow, cached_aid_to_tvdb, create_key_from_series, + BasePopular, MissingTvdbMapping, RecommendedShow, cached_aid_to_tvdb, create_key_from_series, update_recommended_series_cache_index ) @@ -27,17 +27,22 @@ log.logger.addHandler(logging.NullHandler()) -class AnidbPopular(object): # pylint: disable=too-few-public-methods +class AnidbPopular(BasePopular): # pylint: disable=too-few-public-methods + + BASE_URL = 'https://anidb.net/perl-bin/animedb.pl?show=anime&aid={aid}' + TITLE = 'Anidb Popular' + CACHE_SUBFOLDER = __name__.split('.')[-1] if '.' in __name__ else __name__ + def __init__(self): """Class retrieves a specified recommended show list from Trakt. List of returned shows is mapped to a RecommendedShow object """ - self.cache_subfolder = __name__.split('.')[-1] if '.' in __name__ else __name__ + self.cache_subfolder = AnidbPopular.CACHE_SUBFOLDER self.session = MedusaSession() - self.recommender = 'Anidb Popular' - self.base_url = 'https://anidb.net/perl-bin/animedb.pl?show=anime&aid={aid}' - self.default_img_src = 'poster.png' + self.recommender = AnidbPopular.TITLE + self.source = EXTERNAL_ANIDB + self.base_url = AnidbPopular.BASE_URL @recommended_series_cache.cache_on_arguments(namespace='anidb', function_key_generator=create_key_from_series) def _create_recommended_show(self, series, storage_key=None): @@ -55,25 +60,24 @@ def _create_recommended_show(self, series, storage_key=None): rec_show = RecommendedShow( self, series.aid, - series.title, + unicode(series.title), INDEXER_TVDBV2, tvdb_id, **{'rating': series.rating_permanent, 'votes': series.count_permanent, 'image_href': self.base_url.format(aid=series.aid), - 'ids': {'tvdb': tvdb_id, - 'aid': series.aid - } - } + 'ids': { + 'tvdb': tvdb_id, + 'aid': series.aid, + 'is_anime': True + } + } ) # Check cache or get and save image use_default = self.default_img_src if not series.picture.url else None rec_show.cache_image(series.picture.url, default=use_default) - # By default pre-configure the show option anime = True - rec_show.is_anime = True - return rec_show def fetch_popular_shows(self, list_type=REQUEST_HOT): diff --git a/medusa/show/recommendations/imdb.py b/medusa/show/recommendations/imdb.py index 974ee538f5..e9197bbcba 100644 --- a/medusa/show/recommendations/imdb.py +++ b/medusa/show/recommendations/imdb.py @@ -6,16 +6,15 @@ import os import posixpath import re -from builtins import object from medusa import helpers from medusa.cache import recommended_series_cache from medusa.imdb import Imdb -from medusa.indexers.indexer_config import INDEXER_TVDBV2 +from medusa.indexers.indexer_config import INDEXER_TVDBV2, EXTERNAL_IMDB from medusa.logger.adapters.style import BraceAdapter from medusa.session.core import MedusaSession from medusa.show.recommendations.recommended import ( - RecommendedShow, cached_get_imdb_series_details, create_key_from_series, + BasePopular, RecommendedShow, cached_get_imdb_series_details, create_key_from_series, update_recommended_series_cache_index ) @@ -27,16 +26,19 @@ log.logger.addHandler(logging.NullHandler()) -class ImdbPopular(object): +class ImdbPopular(BasePopular): """Gets a list of most popular TV series from imdb.""" + TITLE = 'IMDB Popular' + CACHE_SUBFOLDER = __name__.split('.')[-1] if '.' in __name__ else __name__ + BASE_URL = 'https://anidb.net/perl-bin/animedb.pl?show=anime&aid={aid}' + def __init__(self): """Initialize class.""" - self.cache_subfolder = __name__.split('.')[-1] if '.' in __name__ else __name__ - self.session = MedusaSession() + self.cache_subfolder = ImdbPopular.CACHE_SUBFOLDER self.imdb_api = Imdb(session=self.session) - self.recommender = 'IMDB Popular' - self.default_img_src = 'poster.png' + self.recommender = ImdbPopular.TITLE + self.source = EXTERNAL_IMDB @recommended_series_cache.cache_on_arguments(namespace='imdb', function_key_generator=create_key_from_series) def _create_recommended_show(self, series, storage_key=None): diff --git a/medusa/show/recommendations/recommended.py b/medusa/show/recommendations/recommended.py index ef0a3240f2..2df84ca0d7 100644 --- a/medusa/show/recommendations/recommended.py +++ b/medusa/show/recommendations/recommended.py @@ -20,10 +20,10 @@ import logging import os import posixpath -from builtins import object from os.path import join from medusa import ( + db, app, helpers, ) @@ -31,6 +31,7 @@ from medusa.helpers import ensure_list from medusa.imdb import Imdb from medusa.indexers.utils import indexer_id_to_name +from medusa.indexers.indexer_config import EXTERNAL_ANIDB, EXTERNAL_IMDB, EXTERNAL_TRAKT from medusa.logger.adapters.style import BraceAdapter from medusa.session.core import MedusaSession @@ -84,6 +85,16 @@ class MissingTvdbMapping(Exception): """Exception used when a show can't be mapped to a tvdb indexer id.""" +class BasePopular(object): + def __init__(self, recommender=None, source=None, cache_subfoler=None): + """Base class for the recommended show classes (AnidbPopular, TraktPopular, etc).""" + self.session = MedusaSession() + self.recommender = recommender + self.source = source + self.cache_subfolder = 'recommended' + self.default_img_src = 'poster.png' + + class RecommendedShow(object): """Base class for show recommendations.""" @@ -102,6 +113,7 @@ def __init__(self, rec_show_prov, series_id, title, mapped_indexer, mapped_serie :param image_src: the local url to the "cached" image (poster) :param default_img_src: a default image when no poster available """ + self.source = rec_show_prov.source self.recommender = rec_show_prov.recommender self.cache_subfolder = rec_show_prov.cache_subfolder or 'recommended' self.default_img_src = getattr(rec_show_prov, 'default_img_src', '') @@ -110,12 +122,14 @@ def __init__(self, rec_show_prov, series_id, title, mapped_indexer, mapped_serie self.title = title self.mapped_indexer = int(mapped_indexer) self.mapped_indexer_name = indexer_id_to_name(mapped_indexer) - try: - self.mapped_series_id = int(mapped_series_id) - except ValueError: - raise MissingTvdbMapping('Could not parse the indexer_id [%s]' % mapped_series_id) + self.mapped_series_id = series_id + if self.mapped_series_id: + try: + self.mapped_series_id = int(self.mapped_series_id) + except ValueError: + raise MissingTvdbMapping('Could not parse the indexer_id [%s]' % mapped_series_id) - self.rating = show_attr.get('rating') or 0 + self.rating = float(show_attr.get('rating') or 0) self.votes = show_attr.get('votes') if self.votes and not isinstance(self.votes, int): @@ -125,12 +139,13 @@ def __init__(self, rec_show_prov, series_id, title, mapped_indexer, mapped_serie self.image_href = show_attr.get('image_href') self.image_src = show_attr.get('image_src') self.ids = show_attr.get('ids', {}) - self.is_anime = False + self.is_anime = show_attr.get('is_anime', False) - # Check if the show is currently already in the db - self.show_in_list = bool([show.indexerid for show in app.showList - if show.series_id == self.mapped_series_id and - show.indexer == self.mapped_indexer]) + if self.mapped_series_id: + # Check if the show is currently already in the db + self.show_in_list = bool([show.indexerid for show in app.showList + if show.series_id == self.mapped_series_id and + show.indexer == self.mapped_indexer]) self.session = session def cache_image(self, image_url, default=None): @@ -183,6 +198,105 @@ def __str__(self): """Return a string repr of the recommended list.""" return 'Recommended show {0} from recommended list: {1}'.format(self.title, self.recommender) + def save_to_db(self): + """Insert or update the recommended show to db.""" + cache_db_con = db.DBConnection('cache.db') + # Add to db + + existing_show = cache_db_con.select('SELECT recommended_id from RECOMMENDED WHERE source = ? AND series_id = ?', + [self.source, self.series_id]) + if not existing_show: + cache_db_con.action( + 'INSERT INTO recommended ' + ' (source, series_id, default_indexer, title, rating, votes, is_anime, image_href)' + 'VALUES (?,?,?,?,?,?,?,?)', + [self.source, self.series_id, self.mapped_indexer, self.title, self.rating, self.votes, int(self.is_anime), self.image_href] + ) + else: + cache_db_con.action('UPDATE recommended SET default_indexer = ?, title = ?, rating = ?, votes = ?, is_anime = ?, image_href = ? ' + 'WHERE recommended_id = ?', + [self.mapped_indexer, self.title, self.rating, + self.votes, int(self.is_anime), self.image_href, existing_show[0]['recommended_id']]) + + def to_json(self): + """ + Return JSON representation. + """ + + data = {} + data['source'] = self.source + data['cacheSubfolder'] = self.cache_subfolder + data['seriesId'] = self.series_id + data['title'] = self.title + data['mappedIndexer'] = self.mapped_indexer_name + data['mappedSeriesId'] = self.mapped_series_id + data['rating'] = self.rating + data['votes'] = self.votes + data['imageHref'] = self.image_href + data['imageSrc'] = self.image_src + data['externals'] = self.ids + data['isAnime'] = self.is_anime + data['showInLibrary'] = self.show_in_list + + return data + + +def get_recommended_shows(source=None, series_id=None): + """ + Retrieve recommended shows from the db cache/recommended table. + + All shows are transformed to Recommended objects. + :param source: The Indexer or External source ID + :returns: A list of Rcommended objects. + """ + + cache_db_con = db.DBConnection('cache.db') + query = 'SELECT * from recommended {where}' + where = [] + params = [] + + if source: + where.append('source = ?') + params.append(source) + + if series_id: + where.append('series_id = ?') + params.append(series_id) + + shows = cache_db_con.select( + query.format(where='' if not where else ' WHERE ' + ' AND '.join(where)), params + ) + + recommended_shows = [] + from medusa.show.recommendations.anidb import AnidbPopular + from medusa.show.recommendations.imdb import ImdbPopular + from medusa.show.recommendations.trakt import TraktPopular + mapped_source = {EXTERNAL_TRAKT: TraktPopular, EXTERNAL_ANIDB: AnidbPopular, EXTERNAL_IMDB: ImdbPopular} + for show in shows: + + try: + recommended_shows.append( + RecommendedShow( + BasePopular( + recommender=mapped_source.get(show[b'source']).TITLE, + source=show[b'source'], + cache_subfoler=mapped_source.get(show[b'source']).CACHE_SUBFOLDER + ), + show[b'series_id'], + show[b'title'], + show[b'default_indexer'], + None, + **{ + 'rating': show[b'rating'], + 'votes': show[b'votes'], + 'image_href': show[b'image_href'] + } + ) + ) + except Exception as error: + pass + return recommended_shows + @LazyApi.load_anidb_api @recommended_series_cache.cache_on_arguments() diff --git a/medusa/show/recommendations/trakt.py b/medusa/show/recommendations/trakt.py index 8b62af2734..7651656a3a 100644 --- a/medusa/show/recommendations/trakt.py +++ b/medusa/show/recommendations/trakt.py @@ -11,11 +11,11 @@ from medusa.helper.common import try_int from medusa.helper.exceptions import MultipleShowObjectsException from medusa.indexers.indexer_api import indexerApi -from medusa.indexers.indexer_config import INDEXER_TVDBV2 +from medusa.indexers.indexer_config import INDEXER_TVDBV2, EXTERNAL_TRAKT from medusa.logger.adapters.style import BraceAdapter from medusa.show.recommendations import ExpiringList from medusa.show.recommendations.recommended import ( - RecommendedShow, create_key_from_series, update_recommended_series_cache_index + BasePopular, RecommendedShow, create_key_from_series ) from six import binary_type, text_type @@ -32,17 +32,23 @@ missing_posters = ExpiringList(cache_timeout=3600 * 24 * 3) # Cache 3 days -class TraktPopular(object): +class TraktPopular(BasePopular): """This class retrieves a speficed recommended show list from Trakt. The list of returned shows is mapped to a RecommendedShow object """ + TITLE = 'Trakt Popular' + CACHE_SUBFOLDER = __name__.split('.')[-1] if '.' in __name__ else __name__ + BASE_URL = 'http://www.trakt.tv/shows/{0}' + def __init__(self): """Initialize the trakt recommended list object.""" - self.cache_subfolder = __name__.split('.')[-1] if '.' in __name__ else __name__ - self.recommender = 'Trakt Popular' + self.cache_subfolder = TraktPopular.CACHE_SUBFOLDER + self.source = EXTERNAL_TRAKT + self.recommender = TraktPopular.TITLE self.default_img_src = 'trakt-default.png' + self.base_url = TraktPopular.BASE_URL self.tvdb_api_v2 = indexerApi(INDEXER_TVDBV2).indexer() @recommended_series_cache.cache_on_arguments(namespace='trakt', function_key_generator=create_key_from_series) @@ -56,7 +62,7 @@ def _create_recommended_show(self, series, storage_key=None): series['show']['ids']['tvdb'], **{'rating': series['show']['rating'], 'votes': try_int(series['show']['votes'], '0'), - 'image_href': 'http://www.trakt.tv/shows/{0}'.format(series['show']['ids']['slug']), + 'image_href': self.base_url.format(series['show']['ids']['slug']), # Adds like: {'tmdb': 62126, 'tvdb': 304219, 'trakt': 79382, 'imdb': 'tt3322314', # 'tvrage': None, 'slug': 'marvel-s-luke-cage'} 'ids': series['show']['ids'] @@ -172,8 +178,8 @@ def fetch_popular_shows(self, page_url=None, trakt_list=None): except MultipleShowObjectsException: continue - # Update the dogpile index. This will allow us to retrieve all stored dogpile shows from the dbm. - update_recommended_series_cache_index('trakt', [binary_type(s.series_id) for s in trending_shows]) + # # Update the dogpile index. This will allow us to retrieve all stored dogpile shows from the dbm. + # update_recommended_series_cache_index('trakt', [binary_type(s.series_id) for s in trending_shows]) blacklist = app.TRAKT_BLACKLIST_NAME not in '' except TraktException as error: diff --git a/medusa/show_updater.py b/medusa/show_updater.py index 9e01601d30..9f3737d438 100644 --- a/medusa/show_updater.py +++ b/medusa/show_updater.py @@ -234,39 +234,47 @@ def run(self, force=False): # Update recommended shows from trakt, imdb and anidb # recommended shows are dogpilled into cache/recommended.dbm - if app.CACHE_RECOMMEDED_SHOWS: + if app.CACHE_RECOMMENDED_SHOWS: logger.info(u'Started caching recommended shows') - # Cache trakt shows - for page_url in ( - 'shows/trending', - 'shows/popular', - 'shows/anticipated', - 'shows/collected', - 'shows/watched', - 'shows/played', - 'recommendations/shows', - 'calendars/all/shows/new/%s/30' % datetime.date.today().strftime('%Y-%m-%d'), - 'calendars/all/shows/premieres/%s/30' % datetime.date.today().strftime('%Y-%m-%d') - ): + if app.CACHE_RECOMMENDED_TRAKT: + # Cache trakt shows + for page_url in ( + 'shows/trending', + 'shows/popular', + 'shows/anticipated', + 'shows/collected', + 'shows/watched', + 'shows/played', + 'recommendations/shows', + 'calendars/all/shows/new/%s/30' % datetime.date.today().strftime('%Y-%m-%d'), + 'calendars/all/shows/premieres/%s/30' % datetime.date.today().strftime('%Y-%m-%d') + ): + try: + blacklist, trending_shows, removed_from_medusa = TraktPopular().fetch_popular_shows(page_url=page_url) + for show in trending_shows: + show.save_to_db() + except Exception as error: + logger.info(u'Could not get trakt recommended shows for %s because of error: %s', page_url, error) + logger.debug(u'Not bothering getting the other trakt lists') + + if app.CACHE_RECOMMENDED_IMDB: + # Cache imdb shows + try: + shows = ImdbPopular().fetch_popular_shows() + for show in shows: + show.save_to_db() + except (RequestException, Exception) as error: + logger.info(u'Could not get imdb recommended shows because of error: %s', error) + + if app.CACHE_RECOMMENDED_ANIDB: + # Cache anidb shows try: - TraktPopular().fetch_popular_shows(page_url=page_url) + shows = AnidbPopular().fetch_popular_shows(REQUEST_HOT) + for show in shows: + show.save_to_db() except Exception as error: - logger.info(u'Could not get trakt recommended shows for %s because of error: %s', page_url, error) - logger.debug(u'Not bothering getting the other trakt lists') - break - - # Cache imdb shows - try: - ImdbPopular().fetch_popular_shows() - except (RequestException, Exception) as error: - logger.info(u'Could not get imdb recommended shows because of error: %s', error) - - # Cache anidb shows - try: - AnidbPopular().fetch_popular_shows(REQUEST_HOT) - except Exception as error: - logger.info(u'Could not get anidb recommended shows because of error: %s', error) + logger.info(u'Could not get anidb recommended shows because of error: %s', error) logger.info(u'Finished caching recommended shows') diff --git a/themes-default/slim/src/store/index.js b/themes-default/slim/src/store/index.js index 42515bd0f3..c8bc87101b 100644 --- a/themes-default/slim/src/store/index.js +++ b/themes-default/slim/src/store/index.js @@ -9,6 +9,7 @@ import { notifications, notifiers, qualities, + recommended, shows, socket, statuses @@ -35,6 +36,7 @@ const store = new Store({ notifications, notifiers, qualities, + recommended, shows, socket, statuses diff --git a/themes-default/slim/src/store/modules/index.js b/themes-default/slim/src/store/modules/index.js index 71d7612f61..e904f653e2 100644 --- a/themes-default/slim/src/store/modules/index.js +++ b/themes-default/slim/src/store/modules/index.js @@ -5,6 +5,7 @@ export { default as metadata } from './metadata'; export { default as notifications } from './notifications'; export { default as notifiers } from './notifiers'; export { default as qualities } from './qualities'; +export { default as recommended } from './recommended'; export { default as shows } from './shows'; export { default as socket } from './socket'; export { default as statuses } from './statuses'; diff --git a/themes-default/slim/src/store/mutation-types.js b/themes-default/slim/src/store/mutation-types.js index af9af520b6..cfbfa6bab0 100644 --- a/themes-default/slim/src/store/mutation-types.js +++ b/themes-default/slim/src/store/mutation-types.js @@ -14,6 +14,8 @@ const NOTIFICATIONS_ENABLED = '🔔 Notifications Enabled'; const NOTIFICATIONS_DISABLED = '🔔 Notifications Disabled'; const ADD_CONFIG = '⚙️ Config added to store'; const ADD_SHOW = '📺 Show added to store'; +const ADD_RECOMMENDED_SHOW = '📺 Recommended Show added to store'; + export { LOGIN_PENDING, @@ -31,5 +33,6 @@ export { NOTIFICATIONS_ENABLED, NOTIFICATIONS_DISABLED, ADD_CONFIG, - ADD_SHOW + ADD_SHOW, + ADD_RECOMMENDED_SHOW }; diff --git a/themes-default/slim/static/css/style.css b/themes-default/slim/static/css/style.css index fe23dbf69a..a51eef49ac 100644 --- a/themes-default/slim/static/css/style.css +++ b/themes-default/slim/static/css/style.css @@ -1087,7 +1087,7 @@ div.recommended-image { padding-right: 4px; } -.recommeded-show-link-inline { +.recommended-show-link-inline { height: 16px; padding-bottom: 1px; } diff --git a/themes-default/slim/views/addShows_recommended.mako b/themes-default/slim/views/addShows_recommended.mako index 9591a48507..779124dbda 100644 --- a/themes-default/slim/views/addShows_recommended.mako +++ b/themes-default/slim/views/addShows_recommended.mako @@ -132,12 +132,12 @@ window.app = new Vue({

${int(float(cur_rating)*10)}% % if cur_show.is_anime and cur_show.ids.get('aid'): - + % endif % if cur_show.recommender == 'Trakt Popular': - + % endif

diff --git a/themes-default/slim/views/layouts/main.mako b/themes-default/slim/views/layouts/main.mako index 2165518876..af5a08d92d 100644 --- a/themes-default/slim/views/layouts/main.mako +++ b/themes-default/slim/views/layouts/main.mako @@ -98,11 +98,6 @@ - - - - - diff --git a/themes/dark/assets/css/style.css b/themes/dark/assets/css/style.css index fe23dbf69a..a51eef49ac 100644 --- a/themes/dark/assets/css/style.css +++ b/themes/dark/assets/css/style.css @@ -1087,7 +1087,7 @@ div.recommended-image { padding-right: 4px; } -.recommeded-show-link-inline { +.recommended-show-link-inline { height: 16px; padding-bottom: 1px; } diff --git a/themes/dark/assets/js/medusa-runtime.js b/themes/dark/assets/js/medusa-runtime.js index 73b03febb2..f77477375f 100644 --- a/themes/dark/assets/js/medusa-runtime.js +++ b/themes/dark/assets/js/medusa-runtime.js @@ -2956,7 +2956,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue_ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.js\");\n/* harmony import */ var vuex__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vuex */ \"./node_modules/vuex/dist/vuex.esm.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue-native-websocket */ \"./node_modules/vue-native-websocket/dist/build.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _modules__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./modules */ \"./src/store/modules/index.js\");\n/* harmony import */ var _mutation_types__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./mutation-types */ \"./src/store/mutation-types.js\");\n\n\n\n\n\nvar Store = vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"].Store;\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"]);\nvar store = new Store({\n modules: {\n auth: _modules__WEBPACK_IMPORTED_MODULE_3__[\"auth\"],\n config: _modules__WEBPACK_IMPORTED_MODULE_3__[\"config\"],\n defaults: _modules__WEBPACK_IMPORTED_MODULE_3__[\"defaults\"],\n metadata: _modules__WEBPACK_IMPORTED_MODULE_3__[\"metadata\"],\n notifications: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifications\"],\n notifiers: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifiers\"],\n qualities: _modules__WEBPACK_IMPORTED_MODULE_3__[\"qualities\"],\n shows: _modules__WEBPACK_IMPORTED_MODULE_3__[\"shows\"],\n socket: _modules__WEBPACK_IMPORTED_MODULE_3__[\"socket\"],\n statuses: _modules__WEBPACK_IMPORTED_MODULE_3__[\"statuses\"]\n },\n state: {},\n mutations: {},\n getters: {},\n actions: {}\n}); // Keep as a non-arrow function for `this` context.\n\nvar passToStoreHandler = function passToStoreHandler(eventName, event, next) {\n var target = eventName.toUpperCase();\n var eventData = event.data;\n\n if (target === 'SOCKET_ONMESSAGE') {\n var message = JSON.parse(eventData);\n var data = message.data,\n _event = message.event; // Show the notification to the user\n\n if (_event === 'notification') {\n var body = data.body,\n hash = data.hash,\n type = data.type,\n title = data.title;\n window.displayNotification(type, title, body, hash);\n } else if (_event === 'configUpdated') {\n var section = data.section,\n _config = data.config;\n this.store.dispatch('updateConfig', {\n section: section,\n config: _config\n });\n } else {\n window.displayNotification('info', _event, data);\n }\n } // Resume normal 'passToStore' handling\n\n\n next(eventName, event);\n};\n\nvar websocketUrl = function () {\n var _window$location = window.location,\n protocol = _window$location.protocol,\n host = _window$location.host;\n var proto = protocol === 'https:' ? 'wss:' : 'ws:';\n var WSMessageUrl = '/ui';\n var webRoot = document.body.getAttribute('web-root');\n return \"\".concat(proto, \"//\").concat(host).concat(webRoot, \"/ws\").concat(WSMessageUrl);\n}();\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default.a, websocketUrl, {\n store: store,\n format: 'json',\n reconnection: true,\n // (Boolean) whether to reconnect automatically (false)\n reconnectionAttempts: 2,\n // (Number) number of reconnection attempts before giving up (Infinity),\n reconnectionDelay: 1000,\n // (Number) how long to initially wait before attempting a new (1000)\n passToStoreHandler: passToStoreHandler,\n // (Function|) Handler for events triggered by the WebSocket (false)\n mutations: {\n SOCKET_ONOPEN: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONOPEN\"],\n SOCKET_ONCLOSE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONCLOSE\"],\n SOCKET_ONERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONERROR\"],\n SOCKET_ONMESSAGE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONMESSAGE\"],\n SOCKET_RECONNECT: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT\"],\n SOCKET_RECONNECT_ERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT_ERROR\"]\n }\n});\n/* harmony default export */ __webpack_exports__[\"default\"] = (store);\n\n//# sourceURL=webpack:///./src/store/index.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.js\");\n/* harmony import */ var vuex__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vuex */ \"./node_modules/vuex/dist/vuex.esm.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue-native-websocket */ \"./node_modules/vue-native-websocket/dist/build.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _modules__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./modules */ \"./src/store/modules/index.js\");\n/* harmony import */ var _mutation_types__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./mutation-types */ \"./src/store/mutation-types.js\");\n\n\n\n\n\nvar Store = vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"].Store;\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"]);\nvar store = new Store({\n modules: {\n auth: _modules__WEBPACK_IMPORTED_MODULE_3__[\"auth\"],\n config: _modules__WEBPACK_IMPORTED_MODULE_3__[\"config\"],\n defaults: _modules__WEBPACK_IMPORTED_MODULE_3__[\"defaults\"],\n metadata: _modules__WEBPACK_IMPORTED_MODULE_3__[\"metadata\"],\n notifications: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifications\"],\n notifiers: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifiers\"],\n qualities: _modules__WEBPACK_IMPORTED_MODULE_3__[\"qualities\"],\n recommended: _modules__WEBPACK_IMPORTED_MODULE_3__[\"recommended\"],\n shows: _modules__WEBPACK_IMPORTED_MODULE_3__[\"shows\"],\n socket: _modules__WEBPACK_IMPORTED_MODULE_3__[\"socket\"],\n statuses: _modules__WEBPACK_IMPORTED_MODULE_3__[\"statuses\"]\n },\n state: {},\n mutations: {},\n getters: {},\n actions: {}\n}); // Keep as a non-arrow function for `this` context.\n\nvar passToStoreHandler = function passToStoreHandler(eventName, event, next) {\n var target = eventName.toUpperCase();\n var eventData = event.data;\n\n if (target === 'SOCKET_ONMESSAGE') {\n var message = JSON.parse(eventData);\n var data = message.data,\n _event = message.event; // Show the notification to the user\n\n if (_event === 'notification') {\n var body = data.body,\n hash = data.hash,\n type = data.type,\n title = data.title;\n window.displayNotification(type, title, body, hash);\n } else if (_event === 'configUpdated') {\n var section = data.section,\n _config = data.config;\n this.store.dispatch('updateConfig', {\n section: section,\n config: _config\n });\n } else {\n window.displayNotification('info', _event, data);\n }\n } // Resume normal 'passToStore' handling\n\n\n next(eventName, event);\n};\n\nvar websocketUrl = function () {\n var _window$location = window.location,\n protocol = _window$location.protocol,\n host = _window$location.host;\n var proto = protocol === 'https:' ? 'wss:' : 'ws:';\n var WSMessageUrl = '/ui';\n var webRoot = document.body.getAttribute('web-root');\n return \"\".concat(proto, \"//\").concat(host).concat(webRoot, \"/ws\").concat(WSMessageUrl);\n}();\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default.a, websocketUrl, {\n store: store,\n format: 'json',\n reconnection: true,\n // (Boolean) whether to reconnect automatically (false)\n reconnectionAttempts: 2,\n // (Number) number of reconnection attempts before giving up (Infinity),\n reconnectionDelay: 1000,\n // (Number) how long to initially wait before attempting a new (1000)\n passToStoreHandler: passToStoreHandler,\n // (Function|) Handler for events triggered by the WebSocket (false)\n mutations: {\n SOCKET_ONOPEN: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONOPEN\"],\n SOCKET_ONCLOSE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONCLOSE\"],\n SOCKET_ONERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONERROR\"],\n SOCKET_ONMESSAGE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONMESSAGE\"],\n SOCKET_RECONNECT: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT\"],\n SOCKET_RECONNECT_ERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT_ERROR\"]\n }\n});\n/* harmony default export */ __webpack_exports__[\"default\"] = (store);\n\n//# sourceURL=webpack:///./src/store/index.js?"); /***/ }), @@ -3000,11 +3000,11 @@ eval("__webpack_require__.r(__webpack_exports__);\nvar state = {\n show: {\n /*!************************************!*\ !*** ./src/store/modules/index.js ***! \************************************/ -/*! exports provided: auth, config, defaults, metadata, notifications, notifiers, qualities, shows, socket, statuses */ +/*! exports provided: auth, config, defaults, metadata, notifications, notifiers, qualities, recommended, shows, socket, statuses */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _auth__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./auth */ \"./src/store/modules/auth.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"auth\", function() { return _auth__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; });\n\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./config */ \"./src/store/modules/config.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"config\", function() { return _config__WEBPACK_IMPORTED_MODULE_1__[\"default\"]; });\n\n/* harmony import */ var _defaults__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./defaults */ \"./src/store/modules/defaults.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"defaults\", function() { return _defaults__WEBPACK_IMPORTED_MODULE_2__[\"default\"]; });\n\n/* harmony import */ var _metadata__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./metadata */ \"./src/store/modules/metadata.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"metadata\", function() { return _metadata__WEBPACK_IMPORTED_MODULE_3__[\"default\"]; });\n\n/* harmony import */ var _notifications__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./notifications */ \"./src/store/modules/notifications.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifications\", function() { return _notifications__WEBPACK_IMPORTED_MODULE_4__[\"default\"]; });\n\n/* harmony import */ var _notifiers__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./notifiers */ \"./src/store/modules/notifiers/index.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifiers\", function() { return _notifiers__WEBPACK_IMPORTED_MODULE_5__[\"default\"]; });\n\n/* harmony import */ var _qualities__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./qualities */ \"./src/store/modules/qualities.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"qualities\", function() { return _qualities__WEBPACK_IMPORTED_MODULE_6__[\"default\"]; });\n\n/* harmony import */ var _shows__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./shows */ \"./src/store/modules/shows.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"shows\", function() { return _shows__WEBPACK_IMPORTED_MODULE_7__[\"default\"]; });\n\n/* harmony import */ var _socket__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./socket */ \"./src/store/modules/socket.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"socket\", function() { return _socket__WEBPACK_IMPORTED_MODULE_8__[\"default\"]; });\n\n/* harmony import */ var _statuses__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./statuses */ \"./src/store/modules/statuses.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"statuses\", function() { return _statuses__WEBPACK_IMPORTED_MODULE_9__[\"default\"]; });\n\n\n\n\n\n\n\n\n\n\n\n\n//# sourceURL=webpack:///./src/store/modules/index.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _auth__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./auth */ \"./src/store/modules/auth.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"auth\", function() { return _auth__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; });\n\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./config */ \"./src/store/modules/config.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"config\", function() { return _config__WEBPACK_IMPORTED_MODULE_1__[\"default\"]; });\n\n/* harmony import */ var _defaults__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./defaults */ \"./src/store/modules/defaults.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"defaults\", function() { return _defaults__WEBPACK_IMPORTED_MODULE_2__[\"default\"]; });\n\n/* harmony import */ var _metadata__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./metadata */ \"./src/store/modules/metadata.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"metadata\", function() { return _metadata__WEBPACK_IMPORTED_MODULE_3__[\"default\"]; });\n\n/* harmony import */ var _notifications__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./notifications */ \"./src/store/modules/notifications.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifications\", function() { return _notifications__WEBPACK_IMPORTED_MODULE_4__[\"default\"]; });\n\n/* harmony import */ var _notifiers__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./notifiers */ \"./src/store/modules/notifiers/index.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifiers\", function() { return _notifiers__WEBPACK_IMPORTED_MODULE_5__[\"default\"]; });\n\n/* harmony import */ var _qualities__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./qualities */ \"./src/store/modules/qualities.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"qualities\", function() { return _qualities__WEBPACK_IMPORTED_MODULE_6__[\"default\"]; });\n\n/* harmony import */ var _recommended__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./recommended */ \"./src/store/modules/recommended.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"recommended\", function() { return _recommended__WEBPACK_IMPORTED_MODULE_7__[\"default\"]; });\n\n/* harmony import */ var _shows__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./shows */ \"./src/store/modules/shows.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"shows\", function() { return _shows__WEBPACK_IMPORTED_MODULE_8__[\"default\"]; });\n\n/* harmony import */ var _socket__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./socket */ \"./src/store/modules/socket.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"socket\", function() { return _socket__WEBPACK_IMPORTED_MODULE_9__[\"default\"]; });\n\n/* harmony import */ var _statuses__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./statuses */ \"./src/store/modules/statuses.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"statuses\", function() { return _statuses__WEBPACK_IMPORTED_MODULE_10__[\"default\"]; });\n\n\n\n\n\n\n\n\n\n\n\n\n\n//# sourceURL=webpack:///./src/store/modules/index.js?"); /***/ }), @@ -3308,6 +3308,18 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _mut /***/ }), +/***/ "./src/store/modules/recommended.js": +/*!******************************************!*\ + !*** ./src/store/modules/recommended.js ***! + \******************************************/ +/*! exports provided: default */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _mutation_types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../mutation-types */ \"./src/store/mutation-types.js\");\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\n\nvar state = {\n recommended: []\n};\n\nvar mutations = _defineProperty({}, _mutation_types__WEBPACK_IMPORTED_MODULE_0__[\"ADD_RECOMMENDED_SHOW\"], function (state, show) {\n var existingShow = state.recommended.find(function (_ref) {\n var seriesId = _ref.seriesId,\n source = _ref.source;\n return Number(show.seriesId[show.source]) === Number(seriesId[source]);\n });\n\n if (!existingShow) {\n console.debug(\"Adding \".concat(show.title || show.source + String(show.seriesId), \" as it wasn't found in the shows array\"), show);\n state.recommended.push(show);\n return;\n } // Merge new recommended show object over old one\n // this allows detailed queries to update the record\n // without the non-detailed removing the extra data\n\n\n console.debug(\"Found \".concat(show.title || show.source + String(show.seriesId), \" in shows array attempting merge\"));\n\n var newShow = _objectSpread({}, existingShow, show); // Update state\n\n\n Vue.set(state.recommended, state.recommended.indexOf(existingShow), newShow);\n console.debug(\"Merged \".concat(newShow.title || newShow.source + String(newShow.seriesId)), newShow);\n});\n\nvar getters = {};\nvar actions = {\n /**\r\n * Get recommended shows from API and commit them to the store.\r\n *\r\n * @param {*} context - The store context.\r\n * @param {String} identifier - Identifier for the recommended shows list.\r\n * @Param {String} params - Filter params, for getting a specific recommended list type.\r\n * @returns {(undefined|Promise)} undefined if `shows` was provided or the API response if not.\r\n */\n getRecommendedShows: function getRecommendedShows(context, identifier, params) {\n var commit = context.commit;\n params = {};\n identifier = identifier ? identifier : '';\n return api.get('/recommended/' + identifier, {\n params: params\n }).then(function (res) {\n var shows = res.data;\n return shows.forEach(function (show) {\n commit(_mutation_types__WEBPACK_IMPORTED_MODULE_0__[\"ADD_RECOMMENDED_SHOW\"], show);\n });\n });\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n state: state,\n mutations: mutations,\n getters: getters,\n actions: actions\n});\n\n//# sourceURL=webpack:///./src/store/modules/recommended.js?"); + +/***/ }), + /***/ "./src/store/modules/shows.js": /*!************************************!*\ !*** ./src/store/modules/shows.js ***! @@ -3348,11 +3360,11 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _mut /*!*************************************!*\ !*** ./src/store/mutation-types.js ***! \*************************************/ -/*! exports provided: LOGIN_PENDING, LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, REFRESH_TOKEN, REMOVE_AUTH_ERROR, SOCKET_ONOPEN, SOCKET_ONCLOSE, SOCKET_ONERROR, SOCKET_ONMESSAGE, SOCKET_RECONNECT, SOCKET_RECONNECT_ERROR, NOTIFICATIONS_ENABLED, NOTIFICATIONS_DISABLED, ADD_CONFIG, ADD_SHOW */ +/*! exports provided: LOGIN_PENDING, LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, REFRESH_TOKEN, REMOVE_AUTH_ERROR, SOCKET_ONOPEN, SOCKET_ONCLOSE, SOCKET_ONERROR, SOCKET_ONMESSAGE, SOCKET_RECONNECT, SOCKET_RECONNECT_ERROR, NOTIFICATIONS_ENABLED, NOTIFICATIONS_DISABLED, ADD_CONFIG, ADD_SHOW, ADD_RECOMMENDED_SHOW */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_PENDING\", function() { return LOGIN_PENDING; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_SUCCESS\", function() { return LOGIN_SUCCESS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_FAILED\", function() { return LOGIN_FAILED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGOUT\", function() { return LOGOUT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REFRESH_TOKEN\", function() { return REFRESH_TOKEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REMOVE_AUTH_ERROR\", function() { return REMOVE_AUTH_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONOPEN\", function() { return SOCKET_ONOPEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONCLOSE\", function() { return SOCKET_ONCLOSE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONERROR\", function() { return SOCKET_ONERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONMESSAGE\", function() { return SOCKET_ONMESSAGE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT\", function() { return SOCKET_RECONNECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT_ERROR\", function() { return SOCKET_RECONNECT_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_ENABLED\", function() { return NOTIFICATIONS_ENABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_DISABLED\", function() { return NOTIFICATIONS_DISABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_CONFIG\", function() { return ADD_CONFIG; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_SHOW\", function() { return ADD_SHOW; });\nvar LOGIN_PENDING = '🔒 Logging in';\nvar LOGIN_SUCCESS = '🔒 ✅ Login Successful';\nvar LOGIN_FAILED = '🔒 ❌ Login Failed';\nvar LOGOUT = '🔒 Logout';\nvar REFRESH_TOKEN = '🔒 Refresh Token';\nvar REMOVE_AUTH_ERROR = '🔒 Remove Auth Error';\nvar SOCKET_ONOPEN = '🔗 ✅ WebSocket connected';\nvar SOCKET_ONCLOSE = '🔗 ❌ WebSocket disconnected';\nvar SOCKET_ONERROR = '🔗 ❌ WebSocket error';\nvar SOCKET_ONMESSAGE = '🔗 ✉️ 📥 WebSocket message received';\nvar SOCKET_RECONNECT = '🔗 🔃 WebSocket reconnecting';\nvar SOCKET_RECONNECT_ERROR = '🔗 🔃 ❌ WebSocket reconnection attempt failed';\nvar NOTIFICATIONS_ENABLED = '🔔 Notifications Enabled';\nvar NOTIFICATIONS_DISABLED = '🔔 Notifications Disabled';\nvar ADD_CONFIG = '⚙️ Config added to store';\nvar ADD_SHOW = '📺 Show added to store';\n\n\n//# sourceURL=webpack:///./src/store/mutation-types.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_PENDING\", function() { return LOGIN_PENDING; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_SUCCESS\", function() { return LOGIN_SUCCESS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_FAILED\", function() { return LOGIN_FAILED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGOUT\", function() { return LOGOUT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REFRESH_TOKEN\", function() { return REFRESH_TOKEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REMOVE_AUTH_ERROR\", function() { return REMOVE_AUTH_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONOPEN\", function() { return SOCKET_ONOPEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONCLOSE\", function() { return SOCKET_ONCLOSE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONERROR\", function() { return SOCKET_ONERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONMESSAGE\", function() { return SOCKET_ONMESSAGE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT\", function() { return SOCKET_RECONNECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT_ERROR\", function() { return SOCKET_RECONNECT_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_ENABLED\", function() { return NOTIFICATIONS_ENABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_DISABLED\", function() { return NOTIFICATIONS_DISABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_CONFIG\", function() { return ADD_CONFIG; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_SHOW\", function() { return ADD_SHOW; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_RECOMMENDED_SHOW\", function() { return ADD_RECOMMENDED_SHOW; });\nvar LOGIN_PENDING = '🔒 Logging in';\nvar LOGIN_SUCCESS = '🔒 ✅ Login Successful';\nvar LOGIN_FAILED = '🔒 ❌ Login Failed';\nvar LOGOUT = '🔒 Logout';\nvar REFRESH_TOKEN = '🔒 Refresh Token';\nvar REMOVE_AUTH_ERROR = '🔒 Remove Auth Error';\nvar SOCKET_ONOPEN = '🔗 ✅ WebSocket connected';\nvar SOCKET_ONCLOSE = '🔗 ❌ WebSocket disconnected';\nvar SOCKET_ONERROR = '🔗 ❌ WebSocket error';\nvar SOCKET_ONMESSAGE = '🔗 ✉️ 📥 WebSocket message received';\nvar SOCKET_RECONNECT = '🔗 🔃 WebSocket reconnecting';\nvar SOCKET_RECONNECT_ERROR = '🔗 🔃 ❌ WebSocket reconnection attempt failed';\nvar NOTIFICATIONS_ENABLED = '🔔 Notifications Enabled';\nvar NOTIFICATIONS_DISABLED = '🔔 Notifications Disabled';\nvar ADD_CONFIG = '⚙️ Config added to store';\nvar ADD_SHOW = '📺 Show added to store';\nvar ADD_RECOMMENDED_SHOW = '📺 Recommended Show added to store';\n\n\n//# sourceURL=webpack:///./src/store/mutation-types.js?"); /***/ }), diff --git a/themes/dark/templates/addShows_recommended.mako b/themes/dark/templates/addShows_recommended.mako index 9591a48507..779124dbda 100644 --- a/themes/dark/templates/addShows_recommended.mako +++ b/themes/dark/templates/addShows_recommended.mako @@ -132,12 +132,12 @@ window.app = new Vue({

${int(float(cur_rating)*10)}% % if cur_show.is_anime and cur_show.ids.get('aid'): - + % endif % if cur_show.recommender == 'Trakt Popular': - + % endif

diff --git a/themes/dark/templates/layouts/main.mako b/themes/dark/templates/layouts/main.mako index 2165518876..af5a08d92d 100644 --- a/themes/dark/templates/layouts/main.mako +++ b/themes/dark/templates/layouts/main.mako @@ -98,11 +98,6 @@ - - - - - diff --git a/themes/light/assets/css/style.css b/themes/light/assets/css/style.css index fe23dbf69a..a51eef49ac 100644 --- a/themes/light/assets/css/style.css +++ b/themes/light/assets/css/style.css @@ -1087,7 +1087,7 @@ div.recommended-image { padding-right: 4px; } -.recommeded-show-link-inline { +.recommended-show-link-inline { height: 16px; padding-bottom: 1px; } diff --git a/themes/light/assets/js/medusa-runtime.js b/themes/light/assets/js/medusa-runtime.js index 73b03febb2..f77477375f 100644 --- a/themes/light/assets/js/medusa-runtime.js +++ b/themes/light/assets/js/medusa-runtime.js @@ -2956,7 +2956,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue_ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.js\");\n/* harmony import */ var vuex__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vuex */ \"./node_modules/vuex/dist/vuex.esm.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue-native-websocket */ \"./node_modules/vue-native-websocket/dist/build.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _modules__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./modules */ \"./src/store/modules/index.js\");\n/* harmony import */ var _mutation_types__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./mutation-types */ \"./src/store/mutation-types.js\");\n\n\n\n\n\nvar Store = vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"].Store;\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"]);\nvar store = new Store({\n modules: {\n auth: _modules__WEBPACK_IMPORTED_MODULE_3__[\"auth\"],\n config: _modules__WEBPACK_IMPORTED_MODULE_3__[\"config\"],\n defaults: _modules__WEBPACK_IMPORTED_MODULE_3__[\"defaults\"],\n metadata: _modules__WEBPACK_IMPORTED_MODULE_3__[\"metadata\"],\n notifications: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifications\"],\n notifiers: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifiers\"],\n qualities: _modules__WEBPACK_IMPORTED_MODULE_3__[\"qualities\"],\n shows: _modules__WEBPACK_IMPORTED_MODULE_3__[\"shows\"],\n socket: _modules__WEBPACK_IMPORTED_MODULE_3__[\"socket\"],\n statuses: _modules__WEBPACK_IMPORTED_MODULE_3__[\"statuses\"]\n },\n state: {},\n mutations: {},\n getters: {},\n actions: {}\n}); // Keep as a non-arrow function for `this` context.\n\nvar passToStoreHandler = function passToStoreHandler(eventName, event, next) {\n var target = eventName.toUpperCase();\n var eventData = event.data;\n\n if (target === 'SOCKET_ONMESSAGE') {\n var message = JSON.parse(eventData);\n var data = message.data,\n _event = message.event; // Show the notification to the user\n\n if (_event === 'notification') {\n var body = data.body,\n hash = data.hash,\n type = data.type,\n title = data.title;\n window.displayNotification(type, title, body, hash);\n } else if (_event === 'configUpdated') {\n var section = data.section,\n _config = data.config;\n this.store.dispatch('updateConfig', {\n section: section,\n config: _config\n });\n } else {\n window.displayNotification('info', _event, data);\n }\n } // Resume normal 'passToStore' handling\n\n\n next(eventName, event);\n};\n\nvar websocketUrl = function () {\n var _window$location = window.location,\n protocol = _window$location.protocol,\n host = _window$location.host;\n var proto = protocol === 'https:' ? 'wss:' : 'ws:';\n var WSMessageUrl = '/ui';\n var webRoot = document.body.getAttribute('web-root');\n return \"\".concat(proto, \"//\").concat(host).concat(webRoot, \"/ws\").concat(WSMessageUrl);\n}();\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default.a, websocketUrl, {\n store: store,\n format: 'json',\n reconnection: true,\n // (Boolean) whether to reconnect automatically (false)\n reconnectionAttempts: 2,\n // (Number) number of reconnection attempts before giving up (Infinity),\n reconnectionDelay: 1000,\n // (Number) how long to initially wait before attempting a new (1000)\n passToStoreHandler: passToStoreHandler,\n // (Function|) Handler for events triggered by the WebSocket (false)\n mutations: {\n SOCKET_ONOPEN: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONOPEN\"],\n SOCKET_ONCLOSE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONCLOSE\"],\n SOCKET_ONERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONERROR\"],\n SOCKET_ONMESSAGE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONMESSAGE\"],\n SOCKET_RECONNECT: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT\"],\n SOCKET_RECONNECT_ERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT_ERROR\"]\n }\n});\n/* harmony default export */ __webpack_exports__[\"default\"] = (store);\n\n//# sourceURL=webpack:///./src/store/index.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.js\");\n/* harmony import */ var vuex__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vuex */ \"./node_modules/vuex/dist/vuex.esm.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue-native-websocket */ \"./node_modules/vue-native-websocket/dist/build.js\");\n/* harmony import */ var vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _modules__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./modules */ \"./src/store/modules/index.js\");\n/* harmony import */ var _mutation_types__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./mutation-types */ \"./src/store/mutation-types.js\");\n\n\n\n\n\nvar Store = vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"].Store;\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vuex__WEBPACK_IMPORTED_MODULE_1__[\"default\"]);\nvar store = new Store({\n modules: {\n auth: _modules__WEBPACK_IMPORTED_MODULE_3__[\"auth\"],\n config: _modules__WEBPACK_IMPORTED_MODULE_3__[\"config\"],\n defaults: _modules__WEBPACK_IMPORTED_MODULE_3__[\"defaults\"],\n metadata: _modules__WEBPACK_IMPORTED_MODULE_3__[\"metadata\"],\n notifications: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifications\"],\n notifiers: _modules__WEBPACK_IMPORTED_MODULE_3__[\"notifiers\"],\n qualities: _modules__WEBPACK_IMPORTED_MODULE_3__[\"qualities\"],\n recommended: _modules__WEBPACK_IMPORTED_MODULE_3__[\"recommended\"],\n shows: _modules__WEBPACK_IMPORTED_MODULE_3__[\"shows\"],\n socket: _modules__WEBPACK_IMPORTED_MODULE_3__[\"socket\"],\n statuses: _modules__WEBPACK_IMPORTED_MODULE_3__[\"statuses\"]\n },\n state: {},\n mutations: {},\n getters: {},\n actions: {}\n}); // Keep as a non-arrow function for `this` context.\n\nvar passToStoreHandler = function passToStoreHandler(eventName, event, next) {\n var target = eventName.toUpperCase();\n var eventData = event.data;\n\n if (target === 'SOCKET_ONMESSAGE') {\n var message = JSON.parse(eventData);\n var data = message.data,\n _event = message.event; // Show the notification to the user\n\n if (_event === 'notification') {\n var body = data.body,\n hash = data.hash,\n type = data.type,\n title = data.title;\n window.displayNotification(type, title, body, hash);\n } else if (_event === 'configUpdated') {\n var section = data.section,\n _config = data.config;\n this.store.dispatch('updateConfig', {\n section: section,\n config: _config\n });\n } else {\n window.displayNotification('info', _event, data);\n }\n } // Resume normal 'passToStore' handling\n\n\n next(eventName, event);\n};\n\nvar websocketUrl = function () {\n var _window$location = window.location,\n protocol = _window$location.protocol,\n host = _window$location.host;\n var proto = protocol === 'https:' ? 'wss:' : 'ws:';\n var WSMessageUrl = '/ui';\n var webRoot = document.body.getAttribute('web-root');\n return \"\".concat(proto, \"//\").concat(host).concat(webRoot, \"/ws\").concat(WSMessageUrl);\n}();\n\nvue__WEBPACK_IMPORTED_MODULE_0__[\"default\"].use(vue_native_websocket__WEBPACK_IMPORTED_MODULE_2___default.a, websocketUrl, {\n store: store,\n format: 'json',\n reconnection: true,\n // (Boolean) whether to reconnect automatically (false)\n reconnectionAttempts: 2,\n // (Number) number of reconnection attempts before giving up (Infinity),\n reconnectionDelay: 1000,\n // (Number) how long to initially wait before attempting a new (1000)\n passToStoreHandler: passToStoreHandler,\n // (Function|) Handler for events triggered by the WebSocket (false)\n mutations: {\n SOCKET_ONOPEN: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONOPEN\"],\n SOCKET_ONCLOSE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONCLOSE\"],\n SOCKET_ONERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONERROR\"],\n SOCKET_ONMESSAGE: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_ONMESSAGE\"],\n SOCKET_RECONNECT: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT\"],\n SOCKET_RECONNECT_ERROR: _mutation_types__WEBPACK_IMPORTED_MODULE_4__[\"SOCKET_RECONNECT_ERROR\"]\n }\n});\n/* harmony default export */ __webpack_exports__[\"default\"] = (store);\n\n//# sourceURL=webpack:///./src/store/index.js?"); /***/ }), @@ -3000,11 +3000,11 @@ eval("__webpack_require__.r(__webpack_exports__);\nvar state = {\n show: {\n /*!************************************!*\ !*** ./src/store/modules/index.js ***! \************************************/ -/*! exports provided: auth, config, defaults, metadata, notifications, notifiers, qualities, shows, socket, statuses */ +/*! exports provided: auth, config, defaults, metadata, notifications, notifiers, qualities, recommended, shows, socket, statuses */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _auth__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./auth */ \"./src/store/modules/auth.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"auth\", function() { return _auth__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; });\n\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./config */ \"./src/store/modules/config.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"config\", function() { return _config__WEBPACK_IMPORTED_MODULE_1__[\"default\"]; });\n\n/* harmony import */ var _defaults__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./defaults */ \"./src/store/modules/defaults.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"defaults\", function() { return _defaults__WEBPACK_IMPORTED_MODULE_2__[\"default\"]; });\n\n/* harmony import */ var _metadata__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./metadata */ \"./src/store/modules/metadata.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"metadata\", function() { return _metadata__WEBPACK_IMPORTED_MODULE_3__[\"default\"]; });\n\n/* harmony import */ var _notifications__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./notifications */ \"./src/store/modules/notifications.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifications\", function() { return _notifications__WEBPACK_IMPORTED_MODULE_4__[\"default\"]; });\n\n/* harmony import */ var _notifiers__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./notifiers */ \"./src/store/modules/notifiers/index.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifiers\", function() { return _notifiers__WEBPACK_IMPORTED_MODULE_5__[\"default\"]; });\n\n/* harmony import */ var _qualities__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./qualities */ \"./src/store/modules/qualities.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"qualities\", function() { return _qualities__WEBPACK_IMPORTED_MODULE_6__[\"default\"]; });\n\n/* harmony import */ var _shows__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./shows */ \"./src/store/modules/shows.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"shows\", function() { return _shows__WEBPACK_IMPORTED_MODULE_7__[\"default\"]; });\n\n/* harmony import */ var _socket__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./socket */ \"./src/store/modules/socket.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"socket\", function() { return _socket__WEBPACK_IMPORTED_MODULE_8__[\"default\"]; });\n\n/* harmony import */ var _statuses__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./statuses */ \"./src/store/modules/statuses.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"statuses\", function() { return _statuses__WEBPACK_IMPORTED_MODULE_9__[\"default\"]; });\n\n\n\n\n\n\n\n\n\n\n\n\n//# sourceURL=webpack:///./src/store/modules/index.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _auth__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./auth */ \"./src/store/modules/auth.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"auth\", function() { return _auth__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; });\n\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./config */ \"./src/store/modules/config.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"config\", function() { return _config__WEBPACK_IMPORTED_MODULE_1__[\"default\"]; });\n\n/* harmony import */ var _defaults__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./defaults */ \"./src/store/modules/defaults.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"defaults\", function() { return _defaults__WEBPACK_IMPORTED_MODULE_2__[\"default\"]; });\n\n/* harmony import */ var _metadata__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./metadata */ \"./src/store/modules/metadata.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"metadata\", function() { return _metadata__WEBPACK_IMPORTED_MODULE_3__[\"default\"]; });\n\n/* harmony import */ var _notifications__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./notifications */ \"./src/store/modules/notifications.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifications\", function() { return _notifications__WEBPACK_IMPORTED_MODULE_4__[\"default\"]; });\n\n/* harmony import */ var _notifiers__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./notifiers */ \"./src/store/modules/notifiers/index.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"notifiers\", function() { return _notifiers__WEBPACK_IMPORTED_MODULE_5__[\"default\"]; });\n\n/* harmony import */ var _qualities__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./qualities */ \"./src/store/modules/qualities.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"qualities\", function() { return _qualities__WEBPACK_IMPORTED_MODULE_6__[\"default\"]; });\n\n/* harmony import */ var _recommended__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./recommended */ \"./src/store/modules/recommended.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"recommended\", function() { return _recommended__WEBPACK_IMPORTED_MODULE_7__[\"default\"]; });\n\n/* harmony import */ var _shows__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./shows */ \"./src/store/modules/shows.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"shows\", function() { return _shows__WEBPACK_IMPORTED_MODULE_8__[\"default\"]; });\n\n/* harmony import */ var _socket__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./socket */ \"./src/store/modules/socket.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"socket\", function() { return _socket__WEBPACK_IMPORTED_MODULE_9__[\"default\"]; });\n\n/* harmony import */ var _statuses__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./statuses */ \"./src/store/modules/statuses.js\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"statuses\", function() { return _statuses__WEBPACK_IMPORTED_MODULE_10__[\"default\"]; });\n\n\n\n\n\n\n\n\n\n\n\n\n\n//# sourceURL=webpack:///./src/store/modules/index.js?"); /***/ }), @@ -3308,6 +3308,18 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _mut /***/ }), +/***/ "./src/store/modules/recommended.js": +/*!******************************************!*\ + !*** ./src/store/modules/recommended.js ***! + \******************************************/ +/*! exports provided: default */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _mutation_types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../mutation-types */ \"./src/store/mutation-types.js\");\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\n\nvar state = {\n recommended: []\n};\n\nvar mutations = _defineProperty({}, _mutation_types__WEBPACK_IMPORTED_MODULE_0__[\"ADD_RECOMMENDED_SHOW\"], function (state, show) {\n var existingShow = state.recommended.find(function (_ref) {\n var seriesId = _ref.seriesId,\n source = _ref.source;\n return Number(show.seriesId[show.source]) === Number(seriesId[source]);\n });\n\n if (!existingShow) {\n console.debug(\"Adding \".concat(show.title || show.source + String(show.seriesId), \" as it wasn't found in the shows array\"), show);\n state.recommended.push(show);\n return;\n } // Merge new recommended show object over old one\n // this allows detailed queries to update the record\n // without the non-detailed removing the extra data\n\n\n console.debug(\"Found \".concat(show.title || show.source + String(show.seriesId), \" in shows array attempting merge\"));\n\n var newShow = _objectSpread({}, existingShow, show); // Update state\n\n\n Vue.set(state.recommended, state.recommended.indexOf(existingShow), newShow);\n console.debug(\"Merged \".concat(newShow.title || newShow.source + String(newShow.seriesId)), newShow);\n});\n\nvar getters = {};\nvar actions = {\n /**\r\n * Get recommended shows from API and commit them to the store.\r\n *\r\n * @param {*} context - The store context.\r\n * @param {String} identifier - Identifier for the recommended shows list.\r\n * @Param {String} params - Filter params, for getting a specific recommended list type.\r\n * @returns {(undefined|Promise)} undefined if `shows` was provided or the API response if not.\r\n */\n getRecommendedShows: function getRecommendedShows(context, identifier, params) {\n var commit = context.commit;\n params = {};\n identifier = identifier ? identifier : '';\n return api.get('/recommended/' + identifier, {\n params: params\n }).then(function (res) {\n var shows = res.data;\n return shows.forEach(function (show) {\n commit(_mutation_types__WEBPACK_IMPORTED_MODULE_0__[\"ADD_RECOMMENDED_SHOW\"], show);\n });\n });\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n state: state,\n mutations: mutations,\n getters: getters,\n actions: actions\n});\n\n//# sourceURL=webpack:///./src/store/modules/recommended.js?"); + +/***/ }), + /***/ "./src/store/modules/shows.js": /*!************************************!*\ !*** ./src/store/modules/shows.js ***! @@ -3348,11 +3360,11 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _mut /*!*************************************!*\ !*** ./src/store/mutation-types.js ***! \*************************************/ -/*! exports provided: LOGIN_PENDING, LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, REFRESH_TOKEN, REMOVE_AUTH_ERROR, SOCKET_ONOPEN, SOCKET_ONCLOSE, SOCKET_ONERROR, SOCKET_ONMESSAGE, SOCKET_RECONNECT, SOCKET_RECONNECT_ERROR, NOTIFICATIONS_ENABLED, NOTIFICATIONS_DISABLED, ADD_CONFIG, ADD_SHOW */ +/*! exports provided: LOGIN_PENDING, LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, REFRESH_TOKEN, REMOVE_AUTH_ERROR, SOCKET_ONOPEN, SOCKET_ONCLOSE, SOCKET_ONERROR, SOCKET_ONMESSAGE, SOCKET_RECONNECT, SOCKET_RECONNECT_ERROR, NOTIFICATIONS_ENABLED, NOTIFICATIONS_DISABLED, ADD_CONFIG, ADD_SHOW, ADD_RECOMMENDED_SHOW */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_PENDING\", function() { return LOGIN_PENDING; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_SUCCESS\", function() { return LOGIN_SUCCESS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_FAILED\", function() { return LOGIN_FAILED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGOUT\", function() { return LOGOUT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REFRESH_TOKEN\", function() { return REFRESH_TOKEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REMOVE_AUTH_ERROR\", function() { return REMOVE_AUTH_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONOPEN\", function() { return SOCKET_ONOPEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONCLOSE\", function() { return SOCKET_ONCLOSE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONERROR\", function() { return SOCKET_ONERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONMESSAGE\", function() { return SOCKET_ONMESSAGE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT\", function() { return SOCKET_RECONNECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT_ERROR\", function() { return SOCKET_RECONNECT_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_ENABLED\", function() { return NOTIFICATIONS_ENABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_DISABLED\", function() { return NOTIFICATIONS_DISABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_CONFIG\", function() { return ADD_CONFIG; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_SHOW\", function() { return ADD_SHOW; });\nvar LOGIN_PENDING = '🔒 Logging in';\nvar LOGIN_SUCCESS = '🔒 ✅ Login Successful';\nvar LOGIN_FAILED = '🔒 ❌ Login Failed';\nvar LOGOUT = '🔒 Logout';\nvar REFRESH_TOKEN = '🔒 Refresh Token';\nvar REMOVE_AUTH_ERROR = '🔒 Remove Auth Error';\nvar SOCKET_ONOPEN = '🔗 ✅ WebSocket connected';\nvar SOCKET_ONCLOSE = '🔗 ❌ WebSocket disconnected';\nvar SOCKET_ONERROR = '🔗 ❌ WebSocket error';\nvar SOCKET_ONMESSAGE = '🔗 ✉️ 📥 WebSocket message received';\nvar SOCKET_RECONNECT = '🔗 🔃 WebSocket reconnecting';\nvar SOCKET_RECONNECT_ERROR = '🔗 🔃 ❌ WebSocket reconnection attempt failed';\nvar NOTIFICATIONS_ENABLED = '🔔 Notifications Enabled';\nvar NOTIFICATIONS_DISABLED = '🔔 Notifications Disabled';\nvar ADD_CONFIG = '⚙️ Config added to store';\nvar ADD_SHOW = '📺 Show added to store';\n\n\n//# sourceURL=webpack:///./src/store/mutation-types.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_PENDING\", function() { return LOGIN_PENDING; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_SUCCESS\", function() { return LOGIN_SUCCESS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGIN_FAILED\", function() { return LOGIN_FAILED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOGOUT\", function() { return LOGOUT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REFRESH_TOKEN\", function() { return REFRESH_TOKEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"REMOVE_AUTH_ERROR\", function() { return REMOVE_AUTH_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONOPEN\", function() { return SOCKET_ONOPEN; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONCLOSE\", function() { return SOCKET_ONCLOSE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONERROR\", function() { return SOCKET_ONERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_ONMESSAGE\", function() { return SOCKET_ONMESSAGE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT\", function() { return SOCKET_RECONNECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SOCKET_RECONNECT_ERROR\", function() { return SOCKET_RECONNECT_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_ENABLED\", function() { return NOTIFICATIONS_ENABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"NOTIFICATIONS_DISABLED\", function() { return NOTIFICATIONS_DISABLED; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_CONFIG\", function() { return ADD_CONFIG; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_SHOW\", function() { return ADD_SHOW; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"ADD_RECOMMENDED_SHOW\", function() { return ADD_RECOMMENDED_SHOW; });\nvar LOGIN_PENDING = '🔒 Logging in';\nvar LOGIN_SUCCESS = '🔒 ✅ Login Successful';\nvar LOGIN_FAILED = '🔒 ❌ Login Failed';\nvar LOGOUT = '🔒 Logout';\nvar REFRESH_TOKEN = '🔒 Refresh Token';\nvar REMOVE_AUTH_ERROR = '🔒 Remove Auth Error';\nvar SOCKET_ONOPEN = '🔗 ✅ WebSocket connected';\nvar SOCKET_ONCLOSE = '🔗 ❌ WebSocket disconnected';\nvar SOCKET_ONERROR = '🔗 ❌ WebSocket error';\nvar SOCKET_ONMESSAGE = '🔗 ✉️ 📥 WebSocket message received';\nvar SOCKET_RECONNECT = '🔗 🔃 WebSocket reconnecting';\nvar SOCKET_RECONNECT_ERROR = '🔗 🔃 ❌ WebSocket reconnection attempt failed';\nvar NOTIFICATIONS_ENABLED = '🔔 Notifications Enabled';\nvar NOTIFICATIONS_DISABLED = '🔔 Notifications Disabled';\nvar ADD_CONFIG = '⚙️ Config added to store';\nvar ADD_SHOW = '📺 Show added to store';\nvar ADD_RECOMMENDED_SHOW = '📺 Recommended Show added to store';\n\n\n//# sourceURL=webpack:///./src/store/mutation-types.js?"); /***/ }), diff --git a/themes/light/templates/addShows_recommended.mako b/themes/light/templates/addShows_recommended.mako index 9591a48507..779124dbda 100644 --- a/themes/light/templates/addShows_recommended.mako +++ b/themes/light/templates/addShows_recommended.mako @@ -132,12 +132,12 @@ window.app = new Vue({

${int(float(cur_rating)*10)}% % if cur_show.is_anime and cur_show.ids.get('aid'): - + % endif % if cur_show.recommender == 'Trakt Popular': - + % endif

diff --git a/themes/light/templates/layouts/main.mako b/themes/light/templates/layouts/main.mako index 2165518876..af5a08d92d 100644 --- a/themes/light/templates/layouts/main.mako +++ b/themes/light/templates/layouts/main.mako @@ -98,11 +98,6 @@ - - - - - From dfd505854015c153fd6c5e9484e78659e03e36de Mon Sep 17 00:00:00 2001 From: P0psicles Date: Sun, 25 Nov 2018 12:45:02 +0100 Subject: [PATCH 03/75] Added templates. --- .../slim/src/store/modules/recommended.js | 62 +++ .../slim/views/addShows_recommended_vue.mako | 485 ++++++++++++++++++ .../templates/addShows_recommended_vue.mako | 485 ++++++++++++++++++ .../templates/addShows_recommended_vue.mako | 485 ++++++++++++++++++ 4 files changed, 1517 insertions(+) create mode 100644 themes-default/slim/src/store/modules/recommended.js create mode 100644 themes-default/slim/views/addShows_recommended_vue.mako create mode 100644 themes/dark/templates/addShows_recommended_vue.mako create mode 100644 themes/light/templates/addShows_recommended_vue.mako diff --git a/themes-default/slim/src/store/modules/recommended.js b/themes-default/slim/src/store/modules/recommended.js new file mode 100644 index 0000000000..e592b5cbf4 --- /dev/null +++ b/themes-default/slim/src/store/modules/recommended.js @@ -0,0 +1,62 @@ +import { ADD_RECOMMENDED_SHOW } from '../mutation-types'; + +const state = { + recommended: [] +}; + +const mutations = { + [ADD_RECOMMENDED_SHOW](state, show) { + const existingShow = state.recommended.find(({ seriesId, source }) => Number(show.seriesId[show.source]) === Number(seriesId[source])); + + if (!existingShow) { + console.debug(`Adding ${show.title || show.source + String(show.seriesId)} as it wasn't found in the shows array`, show); + state.recommended.push(show); + return; + } + + // Merge new recommended show object over old one + // this allows detailed queries to update the record + // without the non-detailed removing the extra data + console.debug(`Found ${show.title || show.source + String(show.seriesId)} in shows array attempting merge`); + const newShow = { + ...existingShow, + ...show + }; + + // Update state + Vue.set(state.recommended, state.recommended.indexOf(existingShow), newShow); + console.debug(`Merged ${newShow.title || newShow.source + String(newShow.seriesId)}`, newShow); + } +}; + +const getters = {}; + +const actions = { + /** + * Get recommended shows from API and commit them to the store. + * + * @param {*} context - The store context. + * @param {String} identifier - Identifier for the recommended shows list. + * @Param {String} params - Filter params, for getting a specific recommended list type. + * @returns {(undefined|Promise)} undefined if `shows` was provided or the API response if not. + */ + getRecommendedShows(context, identifier, params) { + const { commit } = context; + params = {}; + + identifier = identifier ? identifier : ''; + return api.get('/recommended/' + identifier, { params }).then(res => { + const shows = res.data; + return shows.forEach(show => { + commit(ADD_RECOMMENDED_SHOW, show); + }); + }); + } +}; + +export default { + state, + mutations, + getters, + actions +}; diff --git a/themes-default/slim/views/addShows_recommended_vue.mako b/themes-default/slim/views/addShows_recommended_vue.mako new file mode 100644 index 0000000000..c981607268 --- /dev/null +++ b/themes-default/slim/views/addShows_recommended_vue.mako @@ -0,0 +1,485 @@ +<%inherit file="/layouts/main.mako"/> +<%block name="scripts"> + + +<%block name="content"> +
+
+

{{ $route.meta.header }}

+
+
+ + + + + + + + + diff --git a/themes/dark/templates/addShows_recommended_vue.mako b/themes/dark/templates/addShows_recommended_vue.mako new file mode 100644 index 0000000000..c981607268 --- /dev/null +++ b/themes/dark/templates/addShows_recommended_vue.mako @@ -0,0 +1,485 @@ +<%inherit file="/layouts/main.mako"/> +<%block name="scripts"> + + +<%block name="content"> +
+
+

{{ $route.meta.header }}

+
+
+ + + + + + + + + diff --git a/themes/light/templates/addShows_recommended_vue.mako b/themes/light/templates/addShows_recommended_vue.mako new file mode 100644 index 0000000000..c981607268 --- /dev/null +++ b/themes/light/templates/addShows_recommended_vue.mako @@ -0,0 +1,485 @@ +<%inherit file="/layouts/main.mako"/> +<%block name="scripts"> + + +<%block name="content"> +
+
+

{{ $route.meta.header }}

+
+
+ + + + + + + + + From 488f80b6928d59824e57eb99ec2bedfde6d58a55 Mon Sep 17 00:00:00 2001 From: P0psicles Date: Mon, 26 Nov 2018 20:32:55 +0100 Subject: [PATCH 04/75] * Fixed getting recommended shows from Imdb * Fixed isotope using vueisotope. * Added db columns image_src and mapped_series_id. * Moved the save_to_db to the for loop. Making it save after each show. And not wait until all shows have been parsed. * Fixed store. --- medusa/databases/cache_db.py | 6 +- medusa/show/recommendations/anidb.py | 3 +- medusa/show/recommendations/imdb.py | 47 +- medusa/show/recommendations/recommended.py | 27 +- medusa/show/recommendations/trakt.py | 8 +- medusa/show_updater.py | 12 +- themes-default/slim/package.json | 4 +- themes-default/slim/src/app.js | 4 + themes-default/slim/src/index.js | 2 + .../slim/src/store/modules/recommended.js | 12 +- .../slim/src/store/mutation-types.js | 1 - .../slim/views/addShows_recommended_vue.mako | 257 +-- .../slim/views/config_notifications.mako | 1 - themes-default/slim/yarn.lock | 1496 +---------------- themes/dark/assets/js/app.js | 2 +- themes/dark/assets/js/index.js | 2 +- themes/dark/assets/js/medusa-runtime.js | 2 +- themes/dark/assets/js/vendors.js | 179 +- .../templates/addShows_recommended_vue.mako | 257 +-- .../dark/templates/config_notifications.mako | 1 - themes/light/assets/js/app.js | 2 +- themes/light/assets/js/index.js | 2 +- themes/light/assets/js/medusa-runtime.js | 2 +- themes/light/assets/js/vendors.js | 179 +- .../templates/addShows_recommended_vue.mako | 257 +-- .../light/templates/config_notifications.mako | 1 - 26 files changed, 919 insertions(+), 1847 deletions(-) diff --git a/medusa/databases/cache_db.py b/medusa/databases/cache_db.py index 4efcf65810..ca89f6e444 100644 --- a/medusa/databases/cache_db.py +++ b/medusa/databases/cache_db.py @@ -212,11 +212,13 @@ def execute(self): `recommended_id` INTEGER PRIMARY KEY AUTOINCREMENT, `source` INTEGER NOT NULL, `series_id` INTEGER NOT NULL, - `default_indexer` INTEGER, + `mapped_indexer` INTEGER, + `mapped_series_id` INTEGER, `title` TEXT NOT NULL, `rating` NUMERIC, `votes` INTEGER, `is_anime` INTEGER DEFAULT 0, - `image_href` TEXT + `image_href` TEXT, + `image_src` TEXT )""" ) diff --git a/medusa/show/recommendations/anidb.py b/medusa/show/recommendations/anidb.py index 73c532d3f6..daf113cd25 100644 --- a/medusa/show/recommendations/anidb.py +++ b/medusa/show/recommendations/anidb.py @@ -38,8 +38,8 @@ def __init__(self): List of returned shows is mapped to a RecommendedShow object """ + super(AnidbPopular, self).__init__() self.cache_subfolder = AnidbPopular.CACHE_SUBFOLDER - self.session = MedusaSession() self.recommender = AnidbPopular.TITLE self.source = EXTERNAL_ANIDB self.base_url = AnidbPopular.BASE_URL @@ -94,6 +94,7 @@ def fetch_popular_shows(self, list_type=REQUEST_HOT): try: recommended_show = self._create_recommended_show(show, storage_key='anidb_{0}'.format(show.aid)) if recommended_show: + recommended_show.save_to_db() result.append(recommended_show) except MissingTvdbMapping: log.info('Could not parse AniDB show {0}, missing tvdb mapping', show.title) diff --git a/medusa/show/recommendations/imdb.py b/medusa/show/recommendations/imdb.py index e9197bbcba..b6491daf2e 100644 --- a/medusa/show/recommendations/imdb.py +++ b/medusa/show/recommendations/imdb.py @@ -35,6 +35,7 @@ class ImdbPopular(BasePopular): def __init__(self): """Initialize class.""" + super(ImdbPopular, self).__init__() self.cache_subfolder = ImdbPopular.CACHE_SUBFOLDER self.imdb_api = Imdb(session=self.session) self.recommender = ImdbPopular.TITLE @@ -66,16 +67,21 @@ def _create_recommended_show(self, series, storage_key=None): def fetch_popular_shows(self): """Get popular show information from IMDB.""" - popular_shows = [] imdb_result = self.imdb_api.get_popular_shows() - + result = [] for imdb_show in imdb_result['ranks']: series = {} + show_details = None imdb_id = series['imdb_tt'] = imdb_show['id'].strip('/').split('/')[-1] if imdb_id: - show_details = cached_get_imdb_series_details(imdb_id) + try: + show_details = cached_get_imdb_series_details(imdb_id) + except RequestException as error: + log.warning('Could not get show details for {imdb_id} with error: {error!r}', + {'imdb_id': imdb_id, 'error': error}) + if show_details: try: series['year'] = imdb_show.get('year') @@ -88,31 +94,28 @@ def fetch_popular_shows(self): series['votes'] = show_details['ratings'].get('ratingCount', 0) series['outline'] = show_details['plot'].get('outline', {}).get('text') series['rating'] = show_details['ratings'].get('rating', 0) + + if all([series['year'], series['name'], series['imdb_tt']]): + try: + recommended_show = self._create_recommended_show(series, storage_key='imdb_{0}'.format( + series['imdb_tt'] + )) + if recommended_show: + recommended_show.save_to_db() + result.append(recommended_show) + except RequestException: + log.warning( + u'Could not connect to indexers to check if you already have' + u' this show in your library: {show} ({year})', + {'show': series['name'], 'year': series['name']} + ) + except Exception as error: log.warning('Could not parse show {imdb_id} with error: {error!r}', {'imdb_id': imdb_id, 'error': error}) else: continue - if all([series['year'], series['name'], series['imdb_tt']]): - popular_shows.append(series) - - result = [] - for series in popular_shows: - try: - recommended_show = self._create_recommended_show(series, storage_key='imdb_{0}'.format(series['imdb_tt'])) - if recommended_show: - result.append(recommended_show) - except RequestException: - log.warning( - u'Could not connect to indexers to check if you already have' - u' this show in your library: {show} ({year})', - {'show': series['name'], 'year': series['name']} - ) - - # Update the dogpile index. This will allow us to retrieve all stored dogpile shows from the dbm. - update_recommended_series_cache_index('imdb', [binary_type(s.series_id) for s in result]) - return result @staticmethod diff --git a/medusa/show/recommendations/recommended.py b/medusa/show/recommendations/recommended.py index 2df84ca0d7..a4f8fc07ea 100644 --- a/medusa/show/recommendations/recommended.py +++ b/medusa/show/recommendations/recommended.py @@ -122,7 +122,7 @@ def __init__(self, rec_show_prov, series_id, title, mapped_indexer, mapped_serie self.title = title self.mapped_indexer = int(mapped_indexer) self.mapped_indexer_name = indexer_id_to_name(mapped_indexer) - self.mapped_series_id = series_id + self.mapped_series_id = mapped_series_id if self.mapped_series_id: try: self.mapped_series_id = int(self.mapped_series_id) @@ -141,6 +141,7 @@ def __init__(self, rec_show_prov, series_id, title, mapped_indexer, mapped_serie self.ids = show_attr.get('ids', {}) self.is_anime = show_attr.get('is_anime', False) + self.show_in_list = None if self.mapped_series_id: # Check if the show is currently already in the db self.show_in_list = bool([show.indexerid for show in app.showList @@ -208,15 +209,17 @@ def save_to_db(self): if not existing_show: cache_db_con.action( 'INSERT INTO recommended ' - ' (source, series_id, default_indexer, title, rating, votes, is_anime, image_href)' - 'VALUES (?,?,?,?,?,?,?,?)', - [self.source, self.series_id, self.mapped_indexer, self.title, self.rating, self.votes, int(self.is_anime), self.image_href] + ' (source, series_id, mapped_indexer, mapped_series_id, title, rating, votes, is_anime, image_href, image_src) ' + 'VALUES (?,?,?,?,?,?,?,?,?,?)', + [self.source, self.series_id, self.mapped_indexer, self.mapped_series_id, self.title, self.rating, self.votes, + int(self.is_anime), self.image_href, self.image_src] ) else: - cache_db_con.action('UPDATE recommended SET default_indexer = ?, title = ?, rating = ?, votes = ?, is_anime = ?, image_href = ? ' + cache_db_con.action('UPDATE recommended SET mapped_indexer = ?, mapped_series_id = ?, title = ?, rating = ?, votes = ?, ' + 'is_anime = ?, image_href = ?, image_src = ? ' 'WHERE recommended_id = ?', - [self.mapped_indexer, self.title, self.rating, - self.votes, int(self.is_anime), self.image_href, existing_show[0]['recommended_id']]) + [self.mapped_indexer, self.mapped_series_id, self.title, self.rating, + self.votes, int(self.is_anime), self.image_href, self.image_src, existing_show[0]['recommended_id']]) def to_json(self): """ @@ -228,7 +231,8 @@ def to_json(self): data['cacheSubfolder'] = self.cache_subfolder data['seriesId'] = self.series_id data['title'] = self.title - data['mappedIndexer'] = self.mapped_indexer_name + data['mappedIndexer'] = self.mapped_indexer + data['mappedIndexerName'] = self.mapped_indexer_name data['mappedSeriesId'] = self.mapped_series_id data['rating'] = self.rating data['votes'] = self.votes @@ -284,12 +288,13 @@ def get_recommended_shows(source=None, series_id=None): ), show[b'series_id'], show[b'title'], - show[b'default_indexer'], - None, + show[b'mapped_indexer'], + show[b'mapped_series_id'], **{ 'rating': show[b'rating'], 'votes': show[b'votes'], - 'image_href': show[b'image_href'] + 'image_href': show[b'image_href'], + 'image_src': show[b'image_src'] } ) ) diff --git a/medusa/show/recommendations/trakt.py b/medusa/show/recommendations/trakt.py index 7651656a3a..ece8c0487e 100644 --- a/medusa/show/recommendations/trakt.py +++ b/medusa/show/recommendations/trakt.py @@ -44,6 +44,7 @@ class TraktPopular(BasePopular): def __init__(self): """Initialize the trakt recommended list object.""" + super(TraktPopular, self).__init__() self.cache_subfolder = TraktPopular.CACHE_SUBFOLDER self.source = EXTERNAL_TRAKT self.recommender = TraktPopular.TITLE @@ -171,10 +172,11 @@ def fetch_popular_shows(self, page_url=None, trakt_list=None): for s in not_liked_show if s['type'] == 'show'): continue else: - trending_shows.append(self._create_recommended_show( + recommended_show = self._create_recommended_show( show, storage_key='trakt_{0}'.format(show['show']['ids']['trakt']) - )) - + ) + recommended_show.save_to_db() + trending_shows.append(recommended_show) except MultipleShowObjectsException: continue diff --git a/medusa/show_updater.py b/medusa/show_updater.py index 9f3737d438..5347aa3c5d 100644 --- a/medusa/show_updater.py +++ b/medusa/show_updater.py @@ -251,9 +251,7 @@ def run(self, force=False): 'calendars/all/shows/premieres/%s/30' % datetime.date.today().strftime('%Y-%m-%d') ): try: - blacklist, trending_shows, removed_from_medusa = TraktPopular().fetch_popular_shows(page_url=page_url) - for show in trending_shows: - show.save_to_db() + TraktPopular().fetch_popular_shows(page_url=page_url) except Exception as error: logger.info(u'Could not get trakt recommended shows for %s because of error: %s', page_url, error) logger.debug(u'Not bothering getting the other trakt lists') @@ -261,18 +259,14 @@ def run(self, force=False): if app.CACHE_RECOMMENDED_IMDB: # Cache imdb shows try: - shows = ImdbPopular().fetch_popular_shows() - for show in shows: - show.save_to_db() + ImdbPopular().fetch_popular_shows() except (RequestException, Exception) as error: logger.info(u'Could not get imdb recommended shows because of error: %s', error) if app.CACHE_RECOMMENDED_ANIDB: # Cache anidb shows try: - shows = AnidbPopular().fetch_popular_shows(REQUEST_HOT) - for show in shows: - show.save_to_db() + AnidbPopular().fetch_popular_shows(REQUEST_HOT) except Exception as error: logger.info(u'Could not get anidb recommended shows because of error: %s', error) diff --git a/themes-default/slim/package.json b/themes-default/slim/package.json index 1531e93a9c..fe55b90bd6 100644 --- a/themes-default/slim/package.json +++ b/themes-default/slim/package.json @@ -26,7 +26,9 @@ } } }, - "dependencies": {}, + "dependencies": { + "vueisotope": "3.1.2" + }, "devDependencies": { "@babel/core": "7.1.2", "@babel/plugin-proposal-object-rest-spread": "7.0.0", diff --git a/themes-default/slim/src/app.js b/themes-default/slim/src/app.js index 6c01f7119c..55245a3b58 100644 --- a/themes-default/slim/src/app.js +++ b/themes-default/slim/src/app.js @@ -5,8 +5,10 @@ import AsyncComputed from 'vue-async-computed'; import ToggleButton from 'vue-js-toggle-button'; import Snotify from 'vue-snotify'; import Truncate from 'vue-truncate-collapsed'; +import isotope from 'vueisotope'; import store from './store'; import router from './router'; + import { isDevelopment } from './utils'; import { AnidbReleaseGroupUi, @@ -35,6 +37,8 @@ Vue.use(AsyncComputed); Vue.use(ToggleButton); Vue.use(Snotify); +Vue.component('isotope', isotope); + // Load x-template components window.components.forEach(component => { if (isDevelopment) { diff --git a/themes-default/slim/src/index.js b/themes-default/slim/src/index.js index 2cc83311c5..698fd2569b 100644 --- a/themes-default/slim/src/index.js +++ b/themes-default/slim/src/index.js @@ -17,6 +17,7 @@ import Snotify from 'vue-snotify'; import Truncate from 'vue-truncate-collapsed'; import axios from 'axios'; import debounce from 'lodash/debounce'; +import isotope from 'vueisotope'; import store from './store'; import router from './router'; import { isDevelopment } from './utils'; @@ -109,6 +110,7 @@ if (window) { window.components.push(SnatchSelection); window.components.push(StateSwitch); window.components.push(Status); + window.components.push(isotope); } const UTIL = { diff --git a/themes-default/slim/src/store/modules/recommended.js b/themes-default/slim/src/store/modules/recommended.js index e592b5cbf4..340044e63e 100644 --- a/themes-default/slim/src/store/modules/recommended.js +++ b/themes-default/slim/src/store/modules/recommended.js @@ -1,16 +1,20 @@ +import Vue from 'vue'; +import { api } from '../../api'; import { ADD_RECOMMENDED_SHOW } from '../mutation-types'; const state = { - recommended: [] + recommended: { + shows: [] + } }; const mutations = { [ADD_RECOMMENDED_SHOW](state, show) { - const existingShow = state.recommended.find(({ seriesId, source }) => Number(show.seriesId[show.source]) === Number(seriesId[source])); + const existingShow = state.recommended.shows.find(({ seriesId, source }) => Number(show.seriesId[show.source]) === Number(seriesId[source])); if (!existingShow) { console.debug(`Adding ${show.title || show.source + String(show.seriesId)} as it wasn't found in the shows array`, show); - state.recommended.push(show); + state.recommended.shows.push(show); return; } @@ -24,7 +28,7 @@ const mutations = { }; // Update state - Vue.set(state.recommended, state.recommended.indexOf(existingShow), newShow); + Vue.set(state.recommended.shows, state.recommended.shows.indexOf(existingShow), newShow); console.debug(`Merged ${newShow.title || newShow.source + String(newShow.seriesId)}`, newShow); } }; diff --git a/themes-default/slim/src/store/mutation-types.js b/themes-default/slim/src/store/mutation-types.js index cfbfa6bab0..4c3e7b8b02 100644 --- a/themes-default/slim/src/store/mutation-types.js +++ b/themes-default/slim/src/store/mutation-types.js @@ -16,7 +16,6 @@ const ADD_CONFIG = '⚙️ Config added to store'; const ADD_SHOW = '📺 Show added to store'; const ADD_RECOMMENDED_SHOW = '📺 Recommended Show added to store'; - export { LOGIN_PENDING, LOGIN_SUCCESS, diff --git a/themes-default/slim/views/addShows_recommended_vue.mako b/themes-default/slim/views/addShows_recommended_vue.mako index c981607268..4404bebc3c 100644 --- a/themes-default/slim/views/addShows_recommended_vue.mako +++ b/themes-default/slim/views/addShows_recommended_vue.mako @@ -11,23 +11,38 @@ window.app = new Vue({ configLoaded: false, rootDirs: [], options: [ - { text: 'Anidb', value: 'anidb' }, - { text: 'IMDB', value: 'imdb' }, - { text: 'Trakt', value: 'trakt' }, + { text: 'Anidb', value: 11 }, + { text: 'IMDB', value: 10 }, + { text: 'Trakt', value: 12 }, + { text: 'all', value: -1} ], - selected: 'anidb', + selectedList: 11, shows: [], // trakt thing - removedFromMedusa: [] + removedFromMedusa: [], + + // Isotope stuff + selected: null, + option: { + getSortData: { + id: "seriesId" + }, + sortBy : "seriesId", + layoutMode: 'fitRows' + }, + imgLazyLoad: new LazyLoad({ + // Example of options object -> see options section + threshold: 500 + }) }; }, created() { - const { $store } = this; + const { $store, imgLazyLoad } = this; $store.dispatch('getRecommendedShows').then(() =>{ - debugger; this.$nextTick(() => { - $.initRemoteShowGrid(); - $.rootDirCheck(); + // This is needed for now. + imgLazyLoad.update(); + imgLazyLoad.handleScroll(); }); }) }, @@ -52,6 +67,8 @@ window.app = new Vue({ $('#showsort').val('original'); $('#showsortdirection').val('asc'); + // Sorts the shows (Original, name, votes, etc.) + // Remove! Moved to vue. $('#showsort').on('change', function() { let sortCriteria; switch (this.value) { @@ -107,21 +124,10 @@ window.app = new Vue({ }); }; - $.fn.loadRemoteShows = function(path, loadingTxt, errorTxt) { - $(this).html(' ' + loadingTxt); - $(this).load(path + ' #container', function(response, status) { - if (status === 'error') { - $(this).empty().html(errorTxt); - } else { - $.initRemoteShowGrid(); - imgLazyLoad.update(); - imgLazyLoad.handleScroll(); - } - }); - }; - /* * Blacklist a show by series id. + * Used by trakt. + * @TODO: convert to vue method */ $.initBlackListShowById = function() { $(document.body).on('click', 'button[data-blacklist-show]', function(e) { @@ -142,6 +148,7 @@ window.app = new Vue({ /* * Adds show by indexer and indexer_id with a number of optional parameters * The show can be added as an anime show by providing the data attribute: data-isanime="1" + * @TODO: move to vue method. Eventually you want to have this go through the store. */ $.initAddShowById = function() { $(document.body).on('click', 'button[data-add-show]', function(e) { @@ -229,11 +236,14 @@ window.app = new Vue({ $('button[data-add-show]').prop('disabled', false); } }; + // recommended shows // Initialise combos for dirty page refreshes $('#showsort').val('original'); $('#showsortdirection').val('asc'); + // Isotope for trakt recommended shows. + // Remove! const $container = [$('#container')]; $.each($container, function() { this.isotope({ @@ -251,6 +261,7 @@ window.app = new Vue({ }); }); + // Move to vue. $('#showsort').on('change', function() { let sortCriteria; switch (this.value) { @@ -285,6 +296,39 @@ window.app = new Vue({ }); }); + // trending shows + // Cleanest way of not showing the black/whitelist, when there isn't a show to show it for + // Cleanest way of not showing the black/whitelist, when there isn't a show to show it for + // $.updateBlackWhiteList(undefined); + // $('#trendingShows').loadRemoteShows( + // 'addShows/getTrendingShows/?traktList=' + $('#traktList').val(), + // 'Loading trending shows...', + // 'Trakt timed out, refresh page to try again' + // ); + + // $('#traktlistselection').on('change', e => { + // const traktList = e.target.value; + // window.history.replaceState({}, document.title, 'addShows/trendingShows/?traktList=' + traktList); + // // Update the jquery tab hrefs, when switching trakt list. + // $('#trakt-tab-1').attr('href', document.location.href.split('=')[0] + '=' + e.target.value); + // $('#trakt-tab-2').attr('href', document.location.href.split('=')[0] + '=' + e.target.value); + // $('#trendingShows').loadRemoteShows( + // 'addShows/getTrendingShows/?traktList=' + traktList, + // 'Loading trending shows...', + // 'Trakt timed out, refresh page to try again' + // ); + // $('h1.header').text('Trakt ' + $('option[value="' + e.target.value + '"]')[0].innerText); + // }); + + // $.initAddShowById(); + // $.initBlackListShowById(); + // $.rootDirCheck(); + + // // popular shows + // $.initRemoteShowGrid(); + // $.rootDirCheck(); + + // The real vue stuff // This is used to wait for the config to be loaded by the store. this.$once('loaded', () => { @@ -292,53 +336,37 @@ window.app = new Vue({ // Map the state values to local data. debugger; - this.shows = shows.concat(stateShows); + this.shows = stateShows; this.configLoaded = true; }); - // trending shows - // Cleanest way of not showing the black/whitelist, when there isn't a show to show it for - // Cleanest way of not showing the black/whitelist, when there isn't a show to show it for - $.updateBlackWhiteList(undefined); - $('#trendingShows').loadRemoteShows( - 'addShows/getTrendingShows/?traktList=' + $('#traktList').val(), - 'Loading trending shows...', - 'Trakt timed out, refresh page to try again' - ); - - $('#traktlistselection').on('change', e => { - const traktList = e.target.value; - window.history.replaceState({}, document.title, 'addShows/trendingShows/?traktList=' + traktList); - // Update the jquery tab hrefs, when switching trakt list. - $('#trakt-tab-1').attr('href', document.location.href.split('=')[0] + '=' + e.target.value); - $('#trakt-tab-2').attr('href', document.location.href.split('=')[0] + '=' + e.target.value); - $('#trendingShows').loadRemoteShows( - 'addShows/getTrendingShows/?traktList=' + traktList, - 'Loading trending shows...', - 'Trakt timed out, refresh page to try again' - ); - $('h1.header').text('Trakt ' + $('option[value="' + e.target.value + '"]')[0].innerText); - }); - - $.initAddShowById(); - $.initBlackListShowById(); - $.rootDirCheck(); - - // popular shows - $.initRemoteShowGrid(); - $.rootDirCheck(); }, computed: { stateShows() { const { $store } = this; // @omg, I need to use recommended.recommended here, but don't know why? - return $store.state.recommended.recommended; + return $store.state.recommended.recommended.shows; + }, + filteredShowsByList() { + const { shows, selectedList, imgLazyLoad } = this; + + if (selectedList === -1) { + return shows; + } + + this.$nextTick(() => { + // This is needed for now. + imgLazyLoad.update(); + imgLazyLoad.handleScroll(); + }); + return shows.filter(show => show.source === selectedList); } }, methods: { changeRecommendedList() { const { $store, shows } = this; + }, containerClass(show) { let classes = 'recommended-container default-poster'; @@ -347,26 +375,32 @@ window.app = new Vue({ classes += ' show-in-list'; } return classes; + }, + isotopeLayout(evt) { + console.log('isotope Layout loaded'); + imgLazyLoad.update(); + imgLazyLoad.handleScroll(); } } }); <%block name="content"> -
+ +

{{ $route.meta.header }}

+ + - - - -