diff --git a/CHANGELOG.md b/CHANGELOG.md index 86371bc520..b07047b061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ----- +## 0.2.10 (2018-09-09) + +#### Fixes +- Fixed UI bugs in home page (when using "split home in tabs") and status page ([#5126](https://github.com/pymedusa/Medusa/pull/5126) + [#5127](https://github.com/pymedusa/Medusa/pull/5127)) +- Fixed error due to `null` values in the episodes database table ([#5132](https://github.com/pymedusa/Medusa/pull/5132)) +- Fixed extraneous calls to AniDB when navigating to any show's page ([#5166](https://github.com/pymedusa/Medusa/pull/5166)) +- Fixed being unable to start Medusa due to an import error ([#5145](https://github.com/pymedusa/Medusa/pull/5145)) +- Fixed UI bugs on: + - Home page (when using "split home in tabs") ([#5126](https://github.com/pymedusa/Medusa/pull/5126)) + - Status page ([#5127](https://github.com/pymedusa/Medusa/pull/5127)) + - Preview Rename page ([#5169](https://github.com/pymedusa/Medusa/pull/5169)) + - Post Processing Config page - saving `select-list` values incorrectly ([#5165](https://github.com/pymedusa/Medusa/pull/5165)) +- Fixed bug in TorrentLeech provider when fetching multiple pages of results ([#5172](https://github.com/pymedusa/Medusa/pull/5172)) + +----- + ## 0.2.9 (2018-09-06) #### Improvements diff --git a/dredd/api-description.yml b/dredd/api-description.yml index 265ac2e319..0dee1b7ae0 100644 --- a/dredd/api-description.yml +++ b/dredd/api-description.yml @@ -75,6 +75,7 @@ paths: description: Filter series based on paused status type: boolean - $ref: '#/parameters/detailed' + - $ref: '#/parameters/fetch' - $ref: '#/parameters/page' - $ref: '#/parameters/limit' - $ref: '#/parameters/sort' @@ -1829,6 +1830,12 @@ parameters: required: false description: Whether response should contain detailed information type: boolean + fetch: + name: fetch + in: query + required: false + description: Whether response should fetch external information + type: boolean page: name: page in: query diff --git a/ext/backports/__init__.py b/ext/backports/__init__.py deleted file mode 100644 index 3ad9513f40..0000000000 --- a/ext/backports/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) diff --git a/ext/configparser.pth b/ext/configparser.pth new file mode 100644 index 0000000000..781f5d2a63 --- /dev/null +++ b/ext/configparser.pth @@ -0,0 +1 @@ +import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('backports',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('backports', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('backports', [os.path.dirname(p)])));m = m or sys.modules.setdefault('backports', types.ModuleType('backports'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p) diff --git a/ext/readme.md b/ext/readme.md index ee1ebf3a05..2be57a9da4 100644 --- a/ext/readme.md +++ b/ext/readme.md @@ -13,7 +13,7 @@ :: | `chardet` | [3.0.4](https://pypi.org/project/chardet/3.0.4/) | **`medusa`**, `beautifulsoup4`, `feedparser`, `html5lib`, `pysrt`, `requests`, `subliminal` | - :: | `cloudflare-scrape` | pymedusa/[320456e](https://github.com/pymedusa/cloudflare-scrape/tree/320456e8b28cedb807363a7a892b1379db843f66) | **`medusa`** | Module: `cfscrape` :: | configobj.py
`validate.py`
`_version.py` | [5.0.6](https://pypi.org/project/configobj/5.0.6/) | **`medusa`** | - -:: | configparser.py
`backports.configparser` | [3.5.0](https://pypi.org/project/configparser/3.5.0/) | `adba` | - +:: | configparser.py
`configparser.pth`
`backports.configparser` | [3.5.0](https://pypi.org/project/configparser/3.5.0/) | `adba` | `configparser.pth` was renamed from `configparser-3.5.0-py2.7-nspkg.pth` :: | contextlib2.py | [0.5.5](https://pypi.org/project/contextlib2/0.5.5/) | **`medusa`**, `tvdbapiv2`, `vcrpy`(?) | Markers: `python_version < '3.5'` :: | decorator.py | [4.3.0](https://pypi.org/project/decorator/4.3.0/) | `validators` | - :: | `dirtyjson` | [1.0.7](https://pypi.org/project/dirtyjson/1.0.7/) | **`medusa`** | - diff --git a/medusa/__main__.py b/medusa/__main__.py index 7f1be2c483..a93bd909fb 100755 --- a/medusa/__main__.py +++ b/medusa/__main__.py @@ -95,6 +95,34 @@ logger = logging.getLogger(__name__) +def fix_incorrect_list_values(data): + """ + @TODO: Remove this in a future version. + + Due to a bug introduced in v0.2.9, the value might be a string representing a Python dict. + See: https://github.com/pymedusa/Medusa/issues/5155 + + Example: `"{u'id': 0, u'value': u'!sync'}"` to `"!sync"` + """ + import ast + + result = [] + for item in data: + if not item: + continue + if not (item.startswith('{') and item.endswith('}')): + # Simple value, don't do anything to it + result.append(item) + continue + try: + # Get the value: `{u'id': 0, u'value': u'!sync'}` => `!sync` + result.append(ast.literal_eval(item)['value']) + except (SyntaxError, KeyError): + pass + + return result + + class Application(object): """Main application module.""" @@ -604,7 +632,11 @@ def initialize(self, console_logging=True): app.RANDOMIZE_PROVIDERS = bool(check_setting_int(app.CFG, 'General', 'randomize_providers', 0)) app.ALLOW_HIGH_PRIORITY = bool(check_setting_int(app.CFG, 'General', 'allow_high_priority', 1)) app.SKIP_REMOVED_FILES = bool(check_setting_int(app.CFG, 'General', 'skip_removed_files', 0)) + app.ALLOWED_EXTENSIONS = check_setting_list(app.CFG, 'General', 'allowed_extensions', app.ALLOWED_EXTENSIONS) + # @TODO: Remove this in a future version. + app.ALLOWED_EXTENSIONS = fix_incorrect_list_values(app.ALLOWED_EXTENSIONS) + app.USENET_RETENTION = check_setting_int(app.CFG, 'General', 'usenet_retention', 500) app.CACHE_TRIMMING = bool(check_setting_int(app.CFG, 'General', 'cache_trimming', 0)) app.MAX_CACHE_AGE = check_setting_int(app.CFG, 'General', 'max_cache_age', 30) @@ -646,7 +678,11 @@ def initialize(self, console_logging=True): app.MOVE_ASSOCIATED_FILES = bool(check_setting_int(app.CFG, 'General', 'move_associated_files', 0)) app.POSTPONE_IF_SYNC_FILES = bool(check_setting_int(app.CFG, 'General', 'postpone_if_sync_files', 1)) app.POSTPONE_IF_NO_SUBS = bool(check_setting_int(app.CFG, 'General', 'postpone_if_no_subs', 0)) + app.SYNC_FILES = check_setting_list(app.CFG, 'General', 'sync_files', app.SYNC_FILES) + # @TODO: Remove this in a future version. + app.SYNC_FILES = fix_incorrect_list_values(app.SYNC_FILES) + app.NFO_RENAME = bool(check_setting_int(app.CFG, 'General', 'nfo_rename', 1)) app.CREATE_MISSING_SHOW_DIRS = bool(check_setting_int(app.CFG, 'General', 'create_missing_show_dirs', 0)) app.ADD_SHOWS_WO_DIR = bool(check_setting_int(app.CFG, 'General', 'add_shows_wo_dir', 0)) @@ -919,6 +955,8 @@ def initialize(self, console_logging=True): app.NO_RESTART = bool(check_setting_int(app.CFG, 'General', 'no_restart', 0)) app.EXTRA_SCRIPTS = [x.strip() for x in check_setting_list(app.CFG, 'General', 'extra_scripts')] + # @TODO: Remove this in a future version. + app.EXTRA_SCRIPTS = fix_incorrect_list_values(app.EXTRA_SCRIPTS) app.USE_LISTVIEW = bool(check_setting_int(app.CFG, 'General', 'use_listview', 0)) diff --git a/medusa/common.py b/medusa/common.py index dd3c6daf0f..1ae55924a6 100644 --- a/medusa/common.py +++ b/medusa/common.py @@ -48,7 +48,7 @@ # To enable, set SPOOF_USER_AGENT = True SPOOF_USER_AGENT = False INSTANCE_ID = str(uuid.uuid1()) -VERSION = '0.2.9' +VERSION = '0.2.10' USER_AGENT = 'Medusa/{version} ({system}; {release}; {instance})'.format( version=VERSION, system=platform.system(), release=platform.release(), instance=INSTANCE_ID) diff --git a/medusa/init/__init__.py b/medusa/init/__init__.py index 36f6dd50aa..e51a7fa0f1 100644 --- a/medusa/init/__init__.py +++ b/medusa/init/__init__.py @@ -8,6 +8,7 @@ import mimetypes import os import shutil +import site import sys @@ -46,8 +47,39 @@ def _ext_lib_location(): def _configure_syspath(): - sys.path.insert(1, _lib_location()) - sys.path.insert(1, _ext_lib_location()) + """Add the vendored libraries into `sys.path`.""" + # Note: These paths will be inserted into `sys.path` in reverse order (LIFO) + # So the last path on this list will be inserted as the first path on `sys.path` + # right after the current working dir. + # For example: [ cwd, pathN, ..., path1, path0, ] + + paths_to_insert = [ + _lib_location(), + _ext_lib_location() + ] + + if sys.version_info[0] == 2: + # Add Python 2-only vendored libraries + paths_to_insert.extend([ + # path_to_lib2, + # path_to_ext2 + ]) + elif sys.version_info[0] == 3: + # Add Python 3-only vendored libraries + paths_to_insert.extend([ + # path_to_lib3, + # path_to_ext3 + ]) + + # Insert paths into `sys.path` and handle `.pth` files + # Inspired by: https://bugs.python.org/issue7744 + for dirpath in paths_to_insert: + # Clear `sys.path` + sys.path, remainder = sys.path[:1], sys.path[1:] + # Add directory as a site-packages directory and handle `.pth` files + site.addsitedir(dirpath) + # Restore rest of `sys.path` + sys.path.extend(remainder) def _register_utf8_codec(): diff --git a/medusa/providers/torrent/json/torrentleech.py b/medusa/providers/torrent/json/torrentleech.py index 89fca0c101..f8ab556858 100644 --- a/medusa/providers/torrent/json/torrentleech.py +++ b/medusa/providers/torrent/json/torrentleech.py @@ -15,6 +15,8 @@ from requests.compat import urljoin from requests.utils import dict_from_cookiejar +from six.moves import range + log = BraceAdapter(logging.getLogger(__name__)) log.logger.addHandler(logging.NullHandler()) @@ -217,7 +219,7 @@ def _pagination(self, data, mode, search_url): ' in your profile options on {name}.', {'name': self.name}) try: - pages = math.ceil(self.max_torrents / per_page) + pages = int(math.ceil(self.max_torrents / per_page)) except ZeroDivisionError: pages = 1 diff --git a/medusa/server/api/v2/series.py b/medusa/server/api/v2/series.py index 9dacd3f755..c13e610cce 100644 --- a/medusa/server/api/v2/series.py +++ b/medusa/server/api/v2/series.py @@ -49,7 +49,11 @@ def filter_series(current): if not series_slug: detailed = self._parse_boolean(self.get_argument('detailed', default=False)) - data = [s.to_json(detailed=detailed) for s in Series.find_series(predicate=filter_series)] + fetch = self._parse_boolean(self.get_argument('fetch', default=False)) + data = [ + s.to_json(detailed=detailed, fetch=fetch) + for s in Series.find_series(predicate=filter_series) + ] return self._paginate(data, sort='title') identifier = SeriesIdentifier.from_slug(series_slug) @@ -61,7 +65,8 @@ def filter_series(current): return self._not_found('Series not found') detailed = self._parse_boolean(self.get_argument('detailed', default=True)) - data = series.to_json(detailed=detailed) + fetch = self._parse_boolean(self.get_argument('fetch', default=False)) + data = series.to_json(detailed=detailed, fetch=fetch) if path_param: if path_param not in data: return self._bad_request("Invalid path parameter '{0}'".format(path_param)) diff --git a/medusa/tv/episode.py b/medusa/tv/episode.py index 666f4165d7..d448ab53e1 100644 --- a/medusa/tv/episode.py +++ b/medusa/tv/episode.py @@ -632,7 +632,7 @@ def load_from_db(self, season, episode): self.airdate = date.fromordinal(int(sql_results[0][b'airdate'])) self.status = int(sql_results[0][b'status'] or UNSET) self.quality = int(sql_results[0][b'quality'] or Quality.NA) - self.watched = int(sql_results[0][b'watched']) + self.watched = bool(sql_results[0][b'watched']) # don't overwrite my location if sql_results[0][b'location']: diff --git a/medusa/tv/series.py b/medusa/tv/series.py index ed52249356..3bd9a881f8 100644 --- a/medusa/tv/series.py +++ b/medusa/tv/series.py @@ -1965,8 +1965,13 @@ def __unicode__(self): to_return += u'anime: {0}\n'.format(self.is_anime) return to_return - def to_json(self, detailed=True): - """Return JSON representation.""" + def to_json(self, detailed=True, fetch=False): + """ + Return JSON representation. + + :param detailed: Append seasons & episodes data as well + :param fetch: Fetch and append external data (for example AniDB release groups) + """ bw_list = self.release_groups or BlackAndWhiteList(self) data = {} @@ -2018,22 +2023,27 @@ def to_json(self, detailed=True): data['config']['defaultEpisodeStatus'] = self.default_ep_status_name data['config']['aliases'] = list(self.aliases) data['config']['release'] = {} - # These are for now considered anime-only options, as they query anidb for available release groups. + data['config']['release']['ignoredWords'] = self.release_ignore_words + data['config']['release']['requiredWords'] = self.release_required_words + + # These are for now considered anime-only options if self.is_anime: data['config']['release']['blacklist'] = bw_list.blacklist data['config']['release']['whitelist'] = bw_list.whitelist - try: - data['config']['release']['allgroups'] = get_release_groups_for_anime(self.name) - except AnidbAdbaConnectionException as error: - data['config']['release']['allgroups'] = [] - log.warning( - 'An anidb adba exception occurred when attempting to get the release groups for the show {show}' - '\nError: {error}', - {'show': self.name, 'error': error} - ) - data['config']['release']['ignoredWords'] = self.release_ignore_words - data['config']['release']['requiredWords'] = self.release_required_words + # Fetch data from external sources + if fetch: + # These are for now considered anime-only options, as they query anidb for available release groups. + if self.is_anime: + try: + data['config']['release']['allgroups'] = get_release_groups_for_anime(self.name) + except AnidbAdbaConnectionException as error: + data['config']['release']['allgroups'] = [] + log.warning( + 'An anidb adba exception occurred when attempting to get the release groups for the show {show}' + '\nError: {error}', + {'show': self.name, 'error': error} + ) if detailed: episodes = self.get_all_episodes() diff --git a/themes-default/slim/src/components/config-post-processing.vue b/themes-default/slim/src/components/config-post-processing.vue index 6102620c97..16e69deec6 100644 --- a/themes-default/slim/src/components/config-post-processing.vue +++ b/themes-default/slim/src/components/config-post-processing.vue @@ -91,7 +91,7 @@ Sync File Extensions
- + comma seperated list of extensions or filename globs Medusa ignores when Post Processing
@@ -154,7 +154,7 @@ Keep associated file extensions
- + Comma seperated list of associated file extensions Medusa should keep while post processing.
Leaving it empty means all associated files will be deleted
@@ -229,7 +229,7 @@ Extra Scripts
- + See Wiki for script arguments description and usage.
@@ -433,6 +433,15 @@ export default { }; }, methods: { + onChangeSyncFiles(items) { + this.postProcessing.syncFiles = items.map(item => item.value); + }, + onChangeAllowedExtensions(items) { + this.postProcessing.allowedExtensions = items.map(item => item.value); + }, + onChangeExtraScripts(items) { + this.postProcessing.extraScripts = items.map(item => item.value); + }, saveNaming(values) { if (!this.configLoaded) { return; diff --git a/themes-default/slim/src/components/select-list.vue b/themes-default/slim/src/components/select-list.vue index 412df16213..36ef78d486 100644 --- a/themes-default/slim/src/components/select-list.vue +++ b/themes-default/slim/src/components/select-list.vue @@ -55,7 +55,6 @@ export default { }, data() { return { - lock: false, editItems: [], newItem: '', indexCounter: 0, @@ -64,20 +63,24 @@ export default { }; }, created() { + /* + These are needed in order to test the component, + but they break the component in the application: + + this.editItems = this.sanitize(this.listItems); + this.csv = this.editItems.map(item => item.value).join(', '); + */ + /** * ListItems property might receive values originating from the API, - * that are sometimes not avaiable when rendering. - * @TODO: Maybe we can remove this in the future. + * that are sometimes not available when rendering. + * @TODO: This is not ideal! Maybe we can remove this in the future. */ const unwatchProp = this.$watch('listItems', () => { unwatchProp(); - this.lock = true; this.editItems = this.sanitize(this.listItems); - this.$nextTick(() => { - this.lock = false; - }); - this.csv = this.editItems.map(x => x.value).join(', '); + this.csv = this.editItems.map(item => item.value).join(', '); }); }, methods: { @@ -138,7 +141,7 @@ export default { } })); } else { - this.csv = this.editItems.map(x => x.value).join(', '); + this.csv = this.editItems.map(item => item.value).join(', '); } }, /** @@ -154,9 +157,7 @@ export default { watch: { editItems: { handler() { - if (!this.lock) { - this.$emit('change', this.editItems); - } + this.$emit('change', this.editItems); }, deep: true }, diff --git a/themes-default/slim/src/store/modules/shows.js b/themes-default/slim/src/store/modules/shows.js index a7453f04b4..ea19625a65 100644 --- a/themes-default/slim/src/store/modules/shows.js +++ b/themes-default/slim/src/store/modules/shows.js @@ -43,17 +43,43 @@ const getters = { } }; +/** + * An object representing request parameters for getting a show from the API. + * + * @typedef {Object} ShowParameteres + * @property {string} indexer - The indexer name (e.g. `tvdb`) + * @property {string} id - The show ID on the indexer (e.g. `12345`) + * @property {boolean} detailed - Whether to fetch detailed information (seasons & episodes) + * @property {boolean} fetch - Whether to fetch external information (for example AniDB release groups) + */ const actions = { - getShow(context, { indexer, id, detailed }) { + /** + * Get show from API and commit it to the store. + * + * @param {*} context - The store context. + * @param {ShowParameteres} parameters - Request parameters. + * @returns {Promise} The API response. + */ + getShow(context, { indexer, id, detailed, fetch }) { const { commit } = context; const params = {}; if (detailed !== undefined) { params.detailed = Boolean(detailed); } + if (fetch !== undefined) { + params.fetch = Boolean(fetch); + } return api.get('/series/' + indexer + id, { params }).then(res => { commit(ADD_SHOW, res.data); }); }, + /** + * Get shows from API and commit them to the store. + * + * @param {*} context - The store context. + * @param {ShowParameteres[]} shows - Shows to get. If not provided, gets the first 1000 shows. + * @returns {(undefined|Promise)} undefined if `shows` was provided or the API response if not. + */ getShows(context, shows) { const { commit, dispatch } = context; diff --git a/themes-default/slim/static/css/style.css b/themes-default/slim/static/css/style.css index e5939cbf91..ee2973d4a3 100644 --- a/themes-default/slim/static/css/style.css +++ b/themes-default/slim/static/css/style.css @@ -543,7 +543,7 @@ div.xem { background-color: rgb(51, 51, 51); } -#container-show, +#container-series, #container-anime { margin: 0 auto; } diff --git a/themes-default/slim/test/specs/select-list.spec.js b/themes-default/slim/test/specs/select-list.spec.js index ff0acf4c36..f40489127a 100644 --- a/themes-default/slim/test/specs/select-list.spec.js +++ b/themes-default/slim/test/specs/select-list.spec.js @@ -20,10 +20,40 @@ test('renders', t => { const { localVue, store } = t.context; const wrapper = mount(SelectList, { localVue, + store, propsData: { listItems: [] - }, - store + } + }); + + t.snapshot(wrapper.html()); +}); + +test.failing('renders with values', t => { + const { localVue, store } = t.context; + + const listItems = [ + 'abc', + 'bcd', + 'test' + ]; + + const wrapper = mount(SelectList, { + localVue, + store, + propsData: { + listItems + } + }); + + const expectedItems = listItems; + const inputWrapperArray = wrapper.findAll('li input[type="text"]'); + + t.is(inputWrapperArray.length, expectedItems.length); + + inputWrapperArray.wrappers.forEach((inputWrapper, index) => { + const { element } = inputWrapper; + t.is(element.value, expectedItems[index]); }); t.snapshot(wrapper.html()); diff --git a/themes-default/slim/test/specs/snapshots/select-list.spec.js.md b/themes-default/slim/test/specs/snapshots/select-list.spec.js.md index f3d5f15afb..948413a96e 100644 --- a/themes-default/slim/test/specs/snapshots/select-list.spec.js.md +++ b/themes-default/slim/test/specs/snapshots/select-list.spec.js.md @@ -9,3 +9,9 @@ Generated by [AVA](https://ava.li). > Snapshot 1 '
' + +## renders with values + +> Snapshot 1 + + '
' diff --git a/themes-default/slim/test/specs/snapshots/select-list.spec.js.snap b/themes-default/slim/test/specs/snapshots/select-list.spec.js.snap index ef99e3c829..b7bd7d7fbb 100644 Binary files a/themes-default/slim/test/specs/snapshots/select-list.spec.js.snap and b/themes-default/slim/test/specs/snapshots/select-list.spec.js.snap differ diff --git a/themes-default/slim/views/addShows_recommended.mako b/themes-default/slim/views/addShows_recommended.mako index 9ec0589069..5027aa84af 100644 --- a/themes-default/slim/views/addShows_recommended.mako +++ b/themes-default/slim/views/addShows_recommended.mako @@ -41,8 +41,8 @@ window.app = new Vue({