diff --git a/CHANGELOG.md b/CHANGELOG.md index 667168e63a..a2ee55df44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased #### New Features +- Add Keep link as post processing method ([7986](https://github.com/pymedusa/Medusa/pull/7986)) #### Improvements - Add show names with dashes to guessit expected titles ([#7918](https://github.com/pymedusa/Medusa/pull/7918)) diff --git a/medusa/post_processor.py b/medusa/post_processor.py index 589a0acf0e..45e208de8e 100644 --- a/medusa/post_processor.py +++ b/medusa/post_processor.py @@ -512,6 +512,17 @@ def symlink(cur_file_path, new_file_path): (cur_file_path, new_file_path, e), logger.ERROR) raise EpisodePostProcessingFailedException('Unable to move and link the files to their new home') + def keeplink(cur_file_path, new_file_path): + self.log(u'Symbolic linking file from {0} to {1}'.format + (cur_file_path, new_file_path), logger.DEBUG) + try: + helpers.symlink(cur_file_path, new_file_path) + helpers.chmod_as_parent(new_file_path) + except (IOError, OSError) as e: + self.log(u'Unable to link file {0} to {1}: {2!r}'.format + (cur_file_path, new_file_path, e), logger.ERROR) + raise EpisodePostProcessingFailedException('Unable to move and link the files to their new home') + def reflink(cur_file_path, new_file_path): self.log(u'Reflink file from {0} to {1}'.format(cur_file_path, new_basename), logger.DEBUG) try: @@ -522,7 +533,7 @@ def reflink(cur_file_path, new_file_path): (cur_file_path, new_file_path, e), logger.ERROR) raise EpisodePostProcessingFailedException('Unable to copy the files to their new home') - action = {'copy': copy, 'move': move, 'hardlink': hardlink, 'symlink': symlink, 'reflink': reflink}.get(self.process_method) + action = {'copy': copy, 'move': move, 'hardlink': hardlink, 'symlink': symlink, 'reflink': reflink, 'keeplink': keeplink}.get(self.process_method) # Subtitle action should be move in case of hardlink|symlink|reflink as downloaded subtitle is not part of torrent subtitle_action = {'copy': copy, 'move': move, 'hardlink': move, 'symlink': move, 'reflink': move}.get(self.process_method) self._combined_file_operation(file_path, new_path, new_basename, associated_files=associated_files, @@ -1229,7 +1240,7 @@ def process(self): try: # do the action to the episode and associated files to the show dir - if self.process_method in ['copy', 'hardlink', 'move', 'symlink', 'reflink']: + if self.process_method in ['copy', 'hardlink', 'move', 'symlink', 'reflink', 'keeplink']: if not self.process_method == 'hardlink': if helpers.is_file_locked(self.file_path, False): raise EpisodePostProcessingFailedException('File is locked for reading') @@ -1302,7 +1313,7 @@ def process(self): self._run_extra_scripts(ep_obj) if not self.nzb_name and all([app.USE_TORRENTS, app.TORRENT_SEED_LOCATION, - self.process_method in ('hardlink', 'symlink', 'reflink')]): + self.process_method in ('hardlink', 'symlink', 'reflink', 'keeplink')]): # Store self.info_hash and self.release_name so later we can remove from client if setting is enabled if self.info_hash: existing_release_names = app.RECENTLY_POSTPROCESSED.get(self.info_hash, []) diff --git a/themes-default/slim/src/components/config-post-processing.vue b/themes-default/slim/src/components/config-post-processing.vue index 6afac331b2..3c231d5f38 100644 --- a/themes-default/slim/src/components/config-post-processing.vue +++ b/themes-default/slim/src/components/config-post-processing.vue @@ -389,7 +389,8 @@ export default { { value: 'copy', text: 'Copy' }, { value: 'move', text: 'Move' }, { value: 'hardlink', text: 'Hard Link' }, - { value: 'symlink', text: 'Symbolic Link' } + { value: 'symlink', text: 'Symbolic Link' }, + { value: 'keeplink', text: 'Keep Link' } ], timezoneOptions: [ { value: 'local', text: 'Local' }, diff --git a/themes-default/slim/test/specs/__snapshots__/config-post-processing.spec.js.snap b/themes-default/slim/test/specs/__snapshots__/config-post-processing.spec.js.snap index 4e5fdd5771..84acb478b3 100644 --- a/themes-default/slim/test/specs/__snapshots__/config-post-processing.spec.js.snap +++ b/themes-default/slim/test/specs/__snapshots__/config-post-processing.spec.js.snap @@ -204,6 +204,11 @@ exports[`ConfigPostProcessing.test.js renders 1`] = ` > Symbolic Link + diff --git a/themes-default/slim/views/home_postprocess.mako b/themes-default/slim/views/home_postprocess.mako index 72caa571f4..61c4d740f3 100644 --- a/themes-default/slim/views/home_postprocess.mako +++ b/themes-default/slim/views/home_postprocess.mako @@ -31,13 +31,13 @@ % if pkgutil.find_loader('reflink') is not None: - <% process_method_text = {'copy': "Copy", 'move': "Move", 'hardlink': "Hard Link", 'symlink' : "Symbolic Link", 'reflink': "Reference Link"} %> - % for cur_action in ('copy', 'move', 'hardlink', 'symlink', 'reflink'): + <% process_method_text = {'copy': "Copy", 'move': "Move", 'hardlink': "Hard Link", 'symlink' : "Symbolic Link", 'reflink': "Reference Link", 'keeplink' : "Keep Link" } %> + % for cur_action in ('copy', 'move', 'hardlink', 'symlink', 'reflink', 'keeplink'): % endfor % else: - <% process_method_text = {'copy': "Copy", 'move': "Move", 'hardlink': "Hard Link", 'symlink' : "Symbolic Link"} %> - % for cur_action in ('copy', 'move', 'hardlink', 'symlink'): + <% process_method_text = {'copy': "Copy", 'move': "Move", 'hardlink': "Hard Link", 'symlink' : "Symbolic Link", 'keeplink' : "Keep Link"} %> + % for cur_action in ('copy', 'move', 'hardlink', 'symlink', 'keeplink'): % endfor % endif diff --git a/themes/light/assets/js/medusa-runtime.js b/themes/light/assets/js/medusa-runtime.js index 51d653f926..05cb2600f1 100644 --- a/themes/light/assets/js/medusa-runtime.js +++ b/themes/light/assets/js/medusa-runtime.js @@ -116,7 +116,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* WEBPACK VAR INJECTION */(f /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* WEBPACK VAR INJECTION */(function($) {/* harmony import */ var vuex__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vuex */ \"./node_modules/vuex/dist/vuex.esm.js\");\n/* harmony import */ var vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vue-js-toggle-button */ \"./node_modules/vue-js-toggle-button/dist/index.js\");\n/* harmony import */ var vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _helpers__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./helpers */ \"./src/components/helpers/index.js\");\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n name: 'config-post-processing',\n components: {\n AppLink: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"AppLink\"],\n FileBrowser: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"FileBrowser\"],\n NamePattern: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"NamePattern\"],\n SelectList: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"SelectList\"],\n ToggleButton: vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1__[\"ToggleButton\"]\n },\n\n data() {\n return {\n presets: [{\n pattern: '%SN - %Sx%0E - %EN',\n example: 'Show Name - 2x03 - Ep Name'\n }, {\n pattern: '%S.N.S%0SE%0E.%E.N',\n example: 'Show.Name.S02E03.Ep.Name'\n }, {\n pattern: '%Sx%0E - %EN',\n example: '2x03 - Ep Name'\n }, {\n pattern: 'S%0SE%0E - %EN',\n example: 'S02E03 - Ep Name'\n }, {\n pattern: 'Season %0S/%S.N.S%0SE%0E.%Q.N-%RG',\n example: 'Season 02/Show.Name.S02E03.720p.HDTV-RLSGROUP'\n }],\n processMethods: [{\n value: 'copy',\n text: 'Copy'\n }, {\n value: 'move',\n text: 'Move'\n }, {\n value: 'hardlink',\n text: 'Hard Link'\n }, {\n value: 'symlink',\n text: 'Symbolic Link'\n }],\n timezoneOptions: [{\n value: 'local',\n text: 'Local'\n }, {\n value: 'network',\n text: 'Network'\n }],\n metadataProviderSelected: null\n };\n },\n\n methods: { ...Object(vuex__WEBPACK_IMPORTED_MODULE_0__[\"mapActions\"])(['setConfig']),\n\n onChangeSyncFiles(items) {\n const {\n postprocessing\n } = this;\n postprocessing.syncFiles = items.map(item => item.value);\n },\n\n onChangeAllowedExtensions(items) {\n const {\n postprocessing\n } = this;\n postprocessing.allowedExtensions = items.map(item => item.value);\n },\n\n onChangeExtraScripts(items) {\n const {\n postprocessing\n } = this;\n postprocessing.extraScripts = items.map(item => item.value);\n },\n\n saveNaming(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.pattern = values.pattern;\n postprocessing.naming.multiEp = values.multiEpStyle;\n },\n\n saveNamingSports(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.patternSports = values.pattern;\n postprocessing.naming.enableCustomNamingSports = values.enabled;\n },\n\n saveNamingAbd(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.patternAirByDate = values.pattern;\n postprocessing.naming.enableCustomNamingAirByDate = values.enabled;\n },\n\n saveNamingAnime(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.patternAnime = values.pattern;\n postprocessing.naming.animeMultiEp = values.multiEpStyle;\n postprocessing.naming.animeNamingType = values.animeNamingType;\n postprocessing.naming.enableCustomNamingAnime = values.enabled;\n },\n\n async save() {\n const {\n postprocessing,\n metadata,\n setConfig\n } = this; // We want to wait until the page has been fully loaded, before starting to save stuff.\n\n if (!this.configLoaded) {\n return;\n } // Disable the save button until we're done.\n\n\n this.saving = true; // Clone the config into a new object\n\n const config = Object.assign({}, {\n postProcessing: postprocessing,\n metadata\n }); // Use destructuring to remove the unwanted keys.\n\n const {\n multiEpStrings,\n reflinkAvailable,\n extraScriptsUrl,\n ...rest\n } = postprocessing; // Assign the object with the keys removed to our copied object.\n\n config.postProcessing = rest;\n const section = 'main';\n\n try {\n await setConfig({\n section,\n config\n });\n this.$snotify.success('Saved Post-Processing config', 'Saved', {\n timeout: 5000\n });\n } catch (error) {\n this.$snotify.error('Error while trying to save Post-Processing config', 'Error');\n } finally {\n this.saving = false;\n }\n },\n\n /**\n * Get the first enabled metadata provider based on enabled features.\n * @param {Object} providers - The metadata providers object.\n * @return {String} - The id of the first enabled provider.\n */\n getFirstEnabledMetadataProvider() {\n const {\n metadata\n } = this;\n const firstEnabledProvider = Object.values(metadata.metadataProviders).find(provider => {\n return provider.showMetadata || provider.episodeMetadata;\n });\n return firstEnabledProvider === undefined ? 'kodi' : firstEnabledProvider.id;\n }\n\n },\n computed: { ...Object(vuex__WEBPACK_IMPORTED_MODULE_0__[\"mapState\"])(['config', 'metadata', 'postprocessing', 'system']),\n\n configLoaded() {\n const {\n postprocessing\n } = this;\n return postprocessing.processAutomatically !== null;\n },\n\n multiEpStringsSelect() {\n const {\n postprocessing\n } = this;\n\n if (!postprocessing.multiEpStrings) {\n return [];\n }\n\n return Object.keys(postprocessing.multiEpStrings).map(k => ({\n value: Number(k),\n text: postprocessing.multiEpStrings[k]\n }));\n }\n\n },\n\n beforeMount() {\n // Wait for the next tick, so the component is rendered\n this.$nextTick(() => {\n $('#config-components').tabs();\n });\n },\n\n watch: {\n 'metadata.metadataProviders': function (providers) {\n // eslint-disable-line object-shorthand\n const {\n getFirstEnabledMetadataProvider\n } = this;\n\n if (Object.keys(providers).length > 0) {\n this.metadataProviderSelected = getFirstEnabledMetadataProvider();\n }\n }\n }\n});\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\")))\n\n//# sourceURL=webpack:///./src/components/config-post-processing.vue?./node_modules/babel-loader/lib??ref--1!./node_modules/vue-loader/lib??vue-loader-options"); +eval("__webpack_require__.r(__webpack_exports__);\n/* WEBPACK VAR INJECTION */(function($) {/* harmony import */ var vuex__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vuex */ \"./node_modules/vuex/dist/vuex.esm.js\");\n/* harmony import */ var vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vue-js-toggle-button */ \"./node_modules/vue-js-toggle-button/dist/index.js\");\n/* harmony import */ var vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _helpers__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./helpers */ \"./src/components/helpers/index.js\");\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n name: 'config-post-processing',\n components: {\n AppLink: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"AppLink\"],\n FileBrowser: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"FileBrowser\"],\n NamePattern: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"NamePattern\"],\n SelectList: _helpers__WEBPACK_IMPORTED_MODULE_2__[\"SelectList\"],\n ToggleButton: vue_js_toggle_button__WEBPACK_IMPORTED_MODULE_1__[\"ToggleButton\"]\n },\n\n data() {\n return {\n presets: [{\n pattern: '%SN - %Sx%0E - %EN',\n example: 'Show Name - 2x03 - Ep Name'\n }, {\n pattern: '%S.N.S%0SE%0E.%E.N',\n example: 'Show.Name.S02E03.Ep.Name'\n }, {\n pattern: '%Sx%0E - %EN',\n example: '2x03 - Ep Name'\n }, {\n pattern: 'S%0SE%0E - %EN',\n example: 'S02E03 - Ep Name'\n }, {\n pattern: 'Season %0S/%S.N.S%0SE%0E.%Q.N-%RG',\n example: 'Season 02/Show.Name.S02E03.720p.HDTV-RLSGROUP'\n }],\n processMethods: [{\n value: 'copy',\n text: 'Copy'\n }, {\n value: 'move',\n text: 'Move'\n }, {\n value: 'hardlink',\n text: 'Hard Link'\n }, {\n value: 'symlink',\n text: 'Symbolic Link'\n }, {\n value: 'keeplink',\n text: 'Keep Link'\n }],\n timezoneOptions: [{\n value: 'local',\n text: 'Local'\n }, {\n value: 'network',\n text: 'Network'\n }],\n metadataProviderSelected: null\n };\n },\n\n methods: { ...Object(vuex__WEBPACK_IMPORTED_MODULE_0__[\"mapActions\"])(['setConfig']),\n\n onChangeSyncFiles(items) {\n const {\n postprocessing\n } = this;\n postprocessing.syncFiles = items.map(item => item.value);\n },\n\n onChangeAllowedExtensions(items) {\n const {\n postprocessing\n } = this;\n postprocessing.allowedExtensions = items.map(item => item.value);\n },\n\n onChangeExtraScripts(items) {\n const {\n postprocessing\n } = this;\n postprocessing.extraScripts = items.map(item => item.value);\n },\n\n saveNaming(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.pattern = values.pattern;\n postprocessing.naming.multiEp = values.multiEpStyle;\n },\n\n saveNamingSports(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.patternSports = values.pattern;\n postprocessing.naming.enableCustomNamingSports = values.enabled;\n },\n\n saveNamingAbd(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.patternAirByDate = values.pattern;\n postprocessing.naming.enableCustomNamingAirByDate = values.enabled;\n },\n\n saveNamingAnime(values) {\n const {\n postprocessing\n } = this;\n\n if (!this.configLoaded) {\n return;\n }\n\n postprocessing.naming.patternAnime = values.pattern;\n postprocessing.naming.animeMultiEp = values.multiEpStyle;\n postprocessing.naming.animeNamingType = values.animeNamingType;\n postprocessing.naming.enableCustomNamingAnime = values.enabled;\n },\n\n async save() {\n const {\n postprocessing,\n metadata,\n setConfig\n } = this; // We want to wait until the page has been fully loaded, before starting to save stuff.\n\n if (!this.configLoaded) {\n return;\n } // Disable the save button until we're done.\n\n\n this.saving = true; // Clone the config into a new object\n\n const config = Object.assign({}, {\n postProcessing: postprocessing,\n metadata\n }); // Use destructuring to remove the unwanted keys.\n\n const {\n multiEpStrings,\n reflinkAvailable,\n extraScriptsUrl,\n ...rest\n } = postprocessing; // Assign the object with the keys removed to our copied object.\n\n config.postProcessing = rest;\n const section = 'main';\n\n try {\n await setConfig({\n section,\n config\n });\n this.$snotify.success('Saved Post-Processing config', 'Saved', {\n timeout: 5000\n });\n } catch (error) {\n this.$snotify.error('Error while trying to save Post-Processing config', 'Error');\n } finally {\n this.saving = false;\n }\n },\n\n /**\n * Get the first enabled metadata provider based on enabled features.\n * @param {Object} providers - The metadata providers object.\n * @return {String} - The id of the first enabled provider.\n */\n getFirstEnabledMetadataProvider() {\n const {\n metadata\n } = this;\n const firstEnabledProvider = Object.values(metadata.metadataProviders).find(provider => {\n return provider.showMetadata || provider.episodeMetadata;\n });\n return firstEnabledProvider === undefined ? 'kodi' : firstEnabledProvider.id;\n }\n\n },\n computed: { ...Object(vuex__WEBPACK_IMPORTED_MODULE_0__[\"mapState\"])(['config', 'metadata', 'postprocessing', 'system']),\n\n configLoaded() {\n const {\n postprocessing\n } = this;\n return postprocessing.processAutomatically !== null;\n },\n\n multiEpStringsSelect() {\n const {\n postprocessing\n } = this;\n\n if (!postprocessing.multiEpStrings) {\n return [];\n }\n\n return Object.keys(postprocessing.multiEpStrings).map(k => ({\n value: Number(k),\n text: postprocessing.multiEpStrings[k]\n }));\n }\n\n },\n\n beforeMount() {\n // Wait for the next tick, so the component is rendered\n this.$nextTick(() => {\n $('#config-components').tabs();\n });\n },\n\n watch: {\n 'metadata.metadataProviders': function (providers) {\n // eslint-disable-line object-shorthand\n const {\n getFirstEnabledMetadataProvider\n } = this;\n\n if (Object.keys(providers).length > 0) {\n this.metadataProviderSelected = getFirstEnabledMetadataProvider();\n }\n }\n }\n});\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\")))\n\n//# sourceURL=webpack:///./src/components/config-post-processing.vue?./node_modules/babel-loader/lib??ref--1!./node_modules/vue-loader/lib??vue-loader-options"); /***/ }), diff --git a/themes/light/templates/home_postprocess.mako b/themes/light/templates/home_postprocess.mako index 72caa571f4..61c4d740f3 100644 --- a/themes/light/templates/home_postprocess.mako +++ b/themes/light/templates/home_postprocess.mako @@ -31,13 +31,13 @@