def _patch_episode(episode, data): """Patch episode and save the changes to DB.""" accepted = {} ignored = {} patches = { 'status': IntegerField(episode, 'status'), 'quality': IntegerField(episode, 'quality'), 'watched': BooleanField(episode, 'watched'), } for key, value in iter_nested_items(data): patch_field = patches.get(key) if patch_field and patch_field.patch(episode, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) # Save patched attributes in db. episode.save_to_db() if ignored: log.warning( 'Episode patch for {episode} ignored {items!r}', { 'episode': episode.identifier, 'items': ignored }, ) return accepted
def patch(self, series_slug, path_param=None): """Patch series.""" if not series_slug: return self._method_not_allowed('Patching multiple series is not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) indexer_id = data.get('id', {}).get(identifier.indexer.slug) if indexer_id is not None and indexer_id != identifier.id: return self._bad_request('Conflicting series identifier') accepted = {} ignored = {} patches = { 'config.aliases': ListField(series, 'aliases'), 'config.defaultEpisodeStatus': StringField(series, 'default_ep_status_name'), 'config.dvdOrder': BooleanField(series, 'dvd_order'), 'config.seasonFolders': BooleanField(series, 'season_folders'), 'config.anime': BooleanField(series, 'anime'), 'config.scene': BooleanField(series, 'scene'), 'config.sports': BooleanField(series, 'sports'), 'config.paused': BooleanField(series, 'paused'), 'config.location': StringField(series, '_location'), 'config.airByDate': BooleanField(series, 'air_by_date'), 'config.subtitlesEnabled': BooleanField(series, 'subtitles'), 'config.release.requiredWords': ListField(series, 'release_required_words'), 'config.release.ignoredWords': ListField(series, 'release_ignore_words'), 'config.release.blacklist': ListField(series, 'blacklist'), 'config.release.whitelist': ListField(series, 'whitelist'), 'language': StringField(series, 'lang'), 'config.qualities.allowed': ListField(series, 'qualities_allowed'), 'config.qualities.preferred': ListField(series, 'qualities_preferred'), 'config.qualities.combined': IntegerField(series, 'quality'), } for key, value in iter_nested_items(data): patch_field = patches.get(key) if patch_field and patch_field.patch(series, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) # Save patched attributes in db. series.save_to_db() if ignored: log.warning('Series patch ignored {items!r}', {'items': ignored}) self._ok(data=accepted)
class ConfigHandler(BaseRequestHandler): """Config request handler.""" #: resource name name = 'config' #: identifier identifier = ('identifier', r'\w+') #: path param path_param = ('path_param', r'\w+') #: allowed HTTP methods allowed_methods = ( 'GET', 'PATCH', ) #: patch mapping patches = { 'anonRedirect': StringField(app, 'ANON_REDIRECT'), 'emby.enabled': BooleanField(app, 'USE_EMBY'), 'torrents.authType': StringField(app, 'TORRENT_AUTH_TYPE'), 'torrents.dir': StringField(app, 'TORRENT_DIR'), 'torrents.enabled': BooleanField(app, 'USE_TORRENTS'), 'torrents.highBandwidth': StringField(app, 'TORRENT_HIGH_BANDWIDTH'), 'torrents.host': StringField(app, 'TORRENT_HOST'), 'torrents.label': StringField(app, 'TORRENT_LABEL'), 'torrents.labelAnime': StringField(app, 'TORRENT_LABEL_ANIME'), 'torrents.method': StringField(app, 'TORRENT_METHOD'), 'torrents.password': StringField(app, 'TORRENT_PASSWORD'), 'torrents.path': BooleanField(app, 'TORRENT_PATH'), 'torrents.paused': BooleanField(app, 'TORRENT_PAUSED'), 'torrents.rpcurl': StringField(app, 'TORRENT_RPCURL'), 'torrents.seedLocation': StringField(app, 'TORRENT_SEED_LOCATION'), 'torrents.seedTime': StringField(app, 'TORRENT_SEED_TIME'), 'torrents.username': StringField(app, 'TORRENT_USERNAME'), 'torrents.verifySSL': BooleanField(app, 'TORRENT_VERIFY_CERT'), 'nzb.enabled': BooleanField(app, 'USE_NZBS'), 'nzb.dir': StringField(app, 'NZB_DIR'), 'nzb.method': StringField(app, 'NZB_METHOD'), 'nzb.nzbget.category': StringField(app, 'NZBGET_CATEGORY'), 'nzb.nzbget.categoryAnime': StringField(app, 'NZBGET_CATEGORY_ANIME'), 'nzb.nzbget.categoryAnimeBacklog': StringField(app, 'NZBGET_CATEGORY_ANIME_BACKLOG'), 'nzb.nzbget.categoryBacklog': StringField(app, 'NZBGET_CATEGORY_BACKLOG'), 'nzb.nzbget.host': StringField(app, 'NZBGET_HOST'), 'nzb.nzbget.password': StringField(app, 'NZBGET_PASSWORD'), 'nzb.nzbget.priority': StringField(app, 'NZBGET_PRIORITY'), 'nzb.nzbget.useHttps': BooleanField(app, 'NZBGET_USE_HTTPS'), 'nzb.nzbget.username': StringField(app, 'NZBGET_USERNAME'), 'nzb.sabnzbd.apiKey': StringField(app, 'SAB_APIKEY'), 'nzb.sabnzbd.category': StringField(app, 'SAB_CATEGORY'), 'nzb.sabnzbd.categoryAnime': StringField(app, 'SAB_CATEGORY_ANIME'), 'nzb.sabnzbd.categoryAnimeBacklog': StringField(app, 'SAB_CATEGORY_ANIME_BACKLOG'), 'nzb.sabnzbd.categoryBacklog': StringField(app, 'SAB_CATEGORY_BACKLOG'), 'nzb.sabnzbd.forced': BooleanField(app, 'SAB_FORCED'), 'nzb.sabnzbd.host': StringField(app, 'SAB_HOST'), 'nzb.sabnzbd.password': StringField(app, 'SAB_PASSWORD'), 'nzb.sabnzbd.username': StringField(app, 'SAB_USERNAME'), 'selectedRootIndex': IntegerField(app, 'SELECTED_ROOT'), 'layout.schedule': EnumField(app, 'COMING_EPS_LAYOUT', ('poster', 'banner', 'list', 'calendar'), default_value='banner', post_processor=layout_schedule_post_processor), 'layout.history': EnumField(app, 'HISTORY_LAYOUT', ('compact', 'detailed'), default_value='detailed'), 'layout.home': EnumField(app, 'HOME_LAYOUT', ('poster', 'small', 'banner', 'simple', 'coverflow'), default_value='poster'), 'layout.show.allSeasons': BooleanField(app, 'DISPLAY_ALL_SEASONS'), 'layout.show.specials': BooleanField(app, 'DISPLAY_SHOW_SPECIALS'), 'layout.show.showListOrder': ListField(app, 'SHOW_LIST_ORDER'), 'theme.name': StringField(app, 'THEME_NAME', setter=theme_name_setter), 'backlogOverview.period': StringField(app, 'BACKLOG_PERIOD'), 'backlogOverview.status': StringField(app, 'BACKLOG_STATUS'), 'rootDirs': ListField(app, 'ROOT_DIRS'), 'showDefaults.status': EnumField(app, 'STATUS_DEFAULT', (SKIPPED, WANTED, IGNORED), int), 'showDefaults.statusAfter': EnumField(app, 'STATUS_DEFAULT_AFTER', (SKIPPED, WANTED, IGNORED), int), 'showDefaults.quality': IntegerField(app, 'QUALITY_DEFAULT', validator=Quality.is_valid_combined_quality), 'showDefaults.subtitles': BooleanField(app, 'SUBTITLES_DEFAULT', validator=lambda v: app.USE_SUBTITLES, converter=bool), 'showDefaults.seasonFolders': BooleanField(app, 'SEASON_FOLDERS_DEFAULT', validator=season_folders_validator, converter=bool), 'showDefaults.anime': BooleanField(app, 'ANIME_DEFAULT', converter=bool), 'showDefaults.scene': BooleanField(app, 'SCENE_DEFAULT', converter=bool), 'postProcessing.showDownloadDir': StringField(app, 'TV_DOWNLOAD_DIR'), 'postProcessing.processAutomatically': BooleanField(app, 'PROCESS_AUTOMATICALLY'), 'postProcessing.processMethod': StringField(app, 'PROCESS_METHOD'), 'postProcessing.deleteRarContent': BooleanField(app, 'DELRARCONTENTS'), 'postProcessing.unpack': BooleanField(app, 'UNPACK'), 'postProcessing.noDelete': BooleanField(app, 'NO_DELETE'), 'postProcessing.postponeIfSyncFiles': BooleanField(app, 'POSTPONE_IF_SYNC_FILES'), 'postProcessing.autoPostprocessorFrequency': IntegerField(app, 'AUTOPOSTPROCESSOR_FREQUENCY'), 'postProcessing.airdateEpisodes': BooleanField(app, 'AIRDATE_EPISODES'), 'postProcessing.moveAssociatedFiles': BooleanField(app, 'MOVE_ASSOCIATED_FILES'), 'postProcessing.allowedExtensions': ListField(app, 'ALLOWED_EXTENSIONS'), 'postProcessing.addShowsWithoutDir': BooleanField(app, 'ADD_SHOWS_WO_DIR'), 'postProcessing.createMissingShowDirs': BooleanField(app, 'CREATE_MISSING_SHOW_DIRS'), 'postProcessing.renameEpisodes': BooleanField(app, 'RENAME_EPISODES'), 'postProcessing.postponeIfNoSubs': BooleanField(app, 'POSTPONE_IF_NO_SUBS'), 'postProcessing.nfoRename': BooleanField(app, 'NFO_RENAME'), 'postProcessing.syncFiles': ListField(app, 'SYNC_FILES'), 'postProcessing.fileTimestampTimezone': StringField(app, 'FILE_TIMESTAMP_TIMEZONE'), 'postProcessing.extraScripts': ListField(app, 'EXTRA_SCRIPTS'), 'postProcessing.extraScriptsUrl': StringField(app, 'EXTRA_SCRIPTS_URL'), 'postProcessing.naming.pattern': StringField(app, 'NAMING_PATTERN'), 'postProcessing.naming.enableCustomNamingAnime': BooleanField(app, 'NAMING_CUSTOM_ANIME'), 'postProcessing.naming.enableCustomNamingSports': BooleanField(app, 'NAMING_CUSTOM_SPORTS'), 'postProcessing.naming.enableCustomNamingAirByDate': BooleanField(app, 'NAMING_CUSTOM_ABD'), 'postProcessing.naming.patternSports': StringField(app, 'NAMING_SPORTS_PATTERN'), 'postProcessing.naming.patternAirByDate': StringField(app, 'NAMING_ABD_PATTERN'), 'postProcessing.naming.patternAnime': StringField(app, 'NAMING_ANIME_PATTERN'), 'postProcessing.naming.animeMultiEp': IntegerField(app, 'NAMING_ANIME_MULTI_EP'), 'postProcessing.naming.animeNamingType': IntegerField(app, 'NAMING_ANIME'), 'postProcessing.naming.multiEp': IntegerField(app, 'NAMING_MULTI_EP'), 'postProcessing.naming.stripYear': BooleanField(app, 'NAMING_STRIP_YEAR'), 'search.general.randomizeProviders': BooleanField(app, 'RANDOMIZE_PROVIDERS'), 'search.general.downloadPropers': BooleanField(app, 'DOWNLOAD_PROPERS'), 'search.general.checkPropersInterval': StringField(app, 'CHECK_PROPERS_INTERVAL'), # 'search.general.propersIntervalLabels': IntegerField(app, 'PROPERS_INTERVAL_LABELS'), 'search.general.propersSearchDays': IntegerField(app, 'PROPERS_SEARCH_DAYS'), 'search.general.backlogDays': IntegerField(app, 'BACKLOG_DAYS'), 'search.general.backlogFrequency': IntegerField(app, 'BACKLOG_FREQUENCY'), 'search.general.minBacklogFrequency': IntegerField(app, 'MIN_BACKLOG_FREQUENCY'), 'search.general.dailySearchFrequency': IntegerField(app, 'DAILYSEARCH_FREQUENCY'), 'search.general.minDailySearchFrequency': IntegerField(app, 'MIN_DAILYSEARCH_FREQUENCY'), 'search.general.removeFromClient': BooleanField(app, 'REMOVE_FROM_CLIENT'), 'search.general.torrentCheckerFrequency': IntegerField(app, 'TORRENT_CHECKER_FREQUENCY'), 'search.general.minTorrentCheckerFrequency': IntegerField(app, 'MIN_TORRENT_CHECKER_FREQUENCY'), 'search.general.usenetRetention': IntegerField(app, 'USENET_RETENTION'), 'search.general.trackersList': ListField(app, 'TRACKERS_LIST'), 'search.general.allowHighPriority': BooleanField(app, 'ALLOW_HIGH_PRIORITY'), 'search.general.useFailedDownloads': BooleanField(app, 'USE_FAILED_DOWNLOADS'), 'search.general.deleteFailed': BooleanField(app, 'DELETE_FAILED'), 'search.general.cacheTrimming': BooleanField(app, 'CACHE_TRIMMING'), 'search.general.maxCacheAge': IntegerField(app, 'MAX_CACHE_AGE'), 'search.filters.ignored': ListField(app, 'IGNORE_WORDS'), 'search.filters.undesired': ListField(app, 'UNDESIRED_WORDS'), 'search.filters.preferred': ListField(app, 'PREFERRED_WORDS'), 'search.filters.required': ListField(app, 'REQUIRE_WORDS'), 'search.filters.ignoredSubsList': ListField(app, 'IGNORED_SUBS_LIST'), 'search.filters.ignoreUnknownSubs': BooleanField(app, 'IGNORE_UND_SUBS'), } def http_get(self, identifier, path_param=None): """Query general configuration. :param identifier: :param path_param: :type path_param: str """ config_sections = DataGenerator.sections() if identifier and identifier not in config_sections: return self._not_found('Config not found') if not identifier: config_data = NonEmptyDict() for section in config_sections: config_data[section] = DataGenerator.get_data(section) return self._ok(data=config_data) config_data = DataGenerator.get_data(identifier) if path_param: if path_param not in config_data: return self._bad_request( '{key} is a invalid path'.format(key=path_param)) config_data = config_data[path_param] return self._ok(data=config_data) def http_patch(self, identifier, *args, **kwargs): """Patch general configuration.""" if not identifier: return self._bad_request('Config identifier not specified') if identifier != 'main': return self._not_found('Config not found') data = json_decode(self.request.body) accepted = {} ignored = {} # Remove the metadata providers from the nested items. # It's ugly but I don't see a better solution for it right now. if data.get('metadata'): metadata_providers = data['metadata'].pop('metadataProviders') if metadata_providers: patch_metadata_providers = MetadataStructureField( app, 'metadata_provider_dict') if patch_metadata_providers and patch_metadata_providers.patch( app, metadata_providers): set_nested_value(accepted, 'metadata.metadataProviders', metadata_providers) else: set_nested_value(ignored, 'metadata.metadataProviders', metadata_providers) for key, value in iter_nested_items(data): patch_field = self.patches.get(key) if patch_field and patch_field.patch(app, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) if ignored: log.warning('Config patch ignored {items!r}', {'items': ignored}) # Make sure to update the config file after everything is updated app.instance.save_config() # Push an update to any open Web UIs through the WebSocket msg = ws.Message('configUpdated', { 'section': identifier, 'config': DataGenerator.get_data(identifier) }) msg.push() return self._ok(data=accepted)
class ConfigHandler(BaseRequestHandler): """Config request handler.""" #: resource name name = 'config' #: identifier identifier = ('identifier', r'\w+') #: path param path_param = ('path_param', r'\w+') #: allowed HTTP methods allowed_methods = ( 'GET', 'PATCH', ) #: patch mapping patches = { 'anonRedirect': StringField(app, 'ANON_REDIRECT'), 'emby.enabled': BooleanField(app, 'USE_EMBY'), 'torrents.enabled': BooleanField(app, 'USE_TORRENTS'), 'torrents.username': StringField(app, 'TORRENT_USERNAME'), 'torrents.password': StringField(app, 'TORRENT_PASSWORD'), 'torrents.label': StringField(app, 'TORRENT_LABEL'), 'torrents.labelAnime': StringField(app, 'TORRENT_LABEL_ANIME'), 'torrents.verifySSL': BooleanField(app, 'TORRENT_VERIFY_CERT'), 'torrents.path': BooleanField(app, 'TORRENT_PATH'), 'selectedRootIndex': IntegerField(app, 'SELECTED_ROOT'), 'layout.schedule': EnumField(app, 'COMING_EPS_LAYOUT', ('poster', 'banner', 'list', 'calendar'), default_value='banner', post_processor=layout_schedule_post_processor), 'layout.history': EnumField(app, 'HISTORY_LAYOUT', ('compact', 'detailed'), default_value='detailed'), 'layout.home': EnumField(app, 'HOME_LAYOUT', ('poster', 'small', 'banner', 'simple', 'coverflow'), default_value='poster'), 'layout.show.allSeasons': BooleanField(app, 'DISPLAY_ALL_SEASONS'), 'layout.show.specials': BooleanField(app, 'DISPLAY_SHOW_SPECIALS'), 'theme.name': StringField(app, 'THEME_NAME'), 'backlogOverview.period': StringField(app, 'BACKLOG_PERIOD'), 'backlogOverview.status': StringField(app, 'BACKLOG_STATUS'), } def get(self, identifier, path_param=None): """Query general configuration. :param identifier: :param path_param: :type path_param: str """ if identifier and identifier != 'main': return self._not_found('Config not found') config_data = NonEmptyDict() config_data['anonRedirect'] = app.ANON_REDIRECT config_data['animeSplitHome'] = app.ANIME_SPLIT_HOME config_data['comingEpsSort'] = app.COMING_EPS_SORT config_data['datePreset'] = app.DATE_PRESET config_data['fuzzyDating'] = app.FUZZY_DATING config_data['themeName'] = app.THEME_NAME config_data['posterSortby'] = app.POSTER_SORTBY config_data['posterSortdir'] = app.POSTER_SORTDIR config_data['rootDirs'] = app.ROOT_DIRS config_data['sortArticle'] = app.SORT_ARTICLE config_data['timePreset'] = app.TIME_PRESET config_data['trimZero'] = app.TRIM_ZERO config_data['fanartBackground'] = app.FANART_BACKGROUND config_data['fanartBackgroundOpacity'] = float( app.FANART_BACKGROUND_OPACITY or 0) config_data['branch'] = app.BRANCH config_data['commitHash'] = app.CUR_COMMIT_HASH config_data['release'] = app.APP_VERSION config_data['sslVersion'] = app.OPENSSL_VERSION config_data['pythonVersion'] = sys.version config_data['databaseVersion'] = NonEmptyDict() config_data['databaseVersion']['major'] = app.MAJOR_DB_VERSION config_data['databaseVersion']['minor'] = app.MINOR_DB_VERSION config_data['os'] = platform.platform() config_data['locale'] = '.'.join( [text_type(loc or 'Unknown') for loc in app.LOCALE]) config_data['localUser'] = app.OS_USER or 'Unknown' config_data['programDir'] = app.PROG_DIR config_data['configFile'] = app.CONFIG_FILE config_data['dbFilename'] = db.dbFilename() config_data['cacheDir'] = app.CACHE_DIR config_data['logDir'] = app.LOG_DIR config_data['appArgs'] = app.MY_ARGS config_data['webRoot'] = app.WEB_ROOT config_data['githubUrl'] = app.GITHUB_IO_URL config_data['wikiUrl'] = app.WIKI_URL config_data['sourceUrl'] = app.APPLICATION_URL config_data['downloadUrl'] = app.DOWNLOAD_URL config_data['subtitlesMulti'] = app.SUBTITLES_MULTI config_data['namingForceFolders'] = app.NAMING_FORCE_FOLDERS config_data['subtitles'] = NonEmptyDict() config_data['subtitles']['enabled'] = bool(app.USE_SUBTITLES) config_data['kodi'] = NonEmptyDict() config_data['kodi']['enabled'] = bool(app.USE_KODI and app.KODI_UPDATE_LIBRARY) config_data['plex'] = NonEmptyDict() config_data['plex']['server'] = NonEmptyDict() config_data['plex']['server']['enabled'] = bool(app.USE_PLEX_SERVER) config_data['plex']['server']['notify'] = NonEmptyDict() config_data['plex']['server']['notify']['snatch'] = bool( app.PLEX_NOTIFY_ONSNATCH) config_data['plex']['server']['notify']['download'] = bool( app.PLEX_NOTIFY_ONDOWNLOAD) config_data['plex']['server']['notify']['subtitleDownload'] = bool( app.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD) config_data['plex']['server']['updateLibrary'] = bool( app.PLEX_UPDATE_LIBRARY) config_data['plex']['server']['host'] = app.PLEX_SERVER_HOST config_data['plex']['server']['token'] = app.PLEX_SERVER_TOKEN config_data['plex']['server']['username'] = app.PLEX_SERVER_USERNAME config_data['plex']['server']['password'] = app.PLEX_SERVER_PASSWORD config_data['plex']['client'] = NonEmptyDict() config_data['plex']['client']['enabled'] = bool(app.USE_PLEX_CLIENT) config_data['plex']['client']['username'] = app.PLEX_CLIENT_USERNAME config_data['plex']['client']['password'] = app.PLEX_CLIENT_PASSWORD config_data['plex']['client']['host'] = app.PLEX_CLIENT_HOST config_data['emby'] = NonEmptyDict() config_data['emby']['enabled'] = bool(app.USE_EMBY) config_data['torrents'] = NonEmptyDict() config_data['torrents']['enabled'] = bool(app.USE_TORRENTS) config_data['torrents']['method'] = app.TORRENT_METHOD config_data['torrents']['username'] = app.TORRENT_USERNAME config_data['torrents']['password'] = app.TORRENT_PASSWORD config_data['torrents']['label'] = app.TORRENT_LABEL config_data['torrents']['labelAnime'] = app.TORRENT_LABEL_ANIME config_data['torrents']['verifySSL'] = app.TORRENT_VERIFY_CERT config_data['torrents']['path'] = app.TORRENT_PATH config_data['torrents']['seedTime'] = app.TORRENT_SEED_TIME config_data['torrents']['paused'] = app.TORRENT_PAUSED config_data['torrents']['highBandwidth'] = app.TORRENT_HIGH_BANDWIDTH config_data['torrents']['host'] = app.TORRENT_HOST config_data['torrents']['rpcurl'] = app.TORRENT_RPCURL config_data['torrents']['authType'] = app.TORRENT_AUTH_TYPE config_data['nzb'] = NonEmptyDict() config_data['nzb']['enabled'] = bool(app.USE_NZBS) config_data['nzb']['username'] = app.NZBGET_USERNAME config_data['nzb']['password'] = app.NZBGET_PASSWORD # app.NZBGET_CATEGORY # app.NZBGET_CATEGORY_BACKLOG # app.NZBGET_CATEGORY_ANIME # app.NZBGET_CATEGORY_ANIME_BACKLOG config_data['nzb']['host'] = app.NZBGET_HOST config_data['nzb']['priority'] = app.NZBGET_PRIORITY config_data['layout'] = NonEmptyDict() config_data['layout']['schedule'] = app.COMING_EPS_LAYOUT config_data['layout']['history'] = app.HISTORY_LAYOUT config_data['layout']['home'] = app.HOME_LAYOUT config_data['layout']['show'] = NonEmptyDict() config_data['layout']['show']['allSeasons'] = bool( app.DISPLAY_ALL_SEASONS) config_data['layout']['show']['specials'] = bool( app.DISPLAY_SHOW_SPECIALS) config_data['selectedRootIndex'] = int( app.SELECTED_ROOT ) if app.SELECTED_ROOT is not None else -1 # All paths config_data['backlogOverview'] = NonEmptyDict() config_data['backlogOverview']['period'] = app.BACKLOG_PERIOD config_data['backlogOverview']['status'] = app.BACKLOG_STATUS if not identifier: return self._paginate([config_data]) if path_param: if path_param not in config_data: return self._bad_request( '{key} is a invalid path'.format(key=path_param)) config_data = config_data[path_param] return self._ok(data=config_data) def patch(self, identifier, *args, **kwargs): """Patch general configuration.""" if not identifier: return self._bad_request('Config identifier not specified') if identifier != 'main': return self._not_found('Config not found') data = json_decode(self.request.body) accepted = {} ignored = {} for key, value in iter_nested_items(data): patch_field = self.patches.get(key) if patch_field and patch_field.patch(app, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) if ignored: log.warning('Config patch ignored %r', ignored) # Make sure to update the config file after everything is updated app.instance.save_config() self._ok(data=accepted)
class ConfigHandler(BaseRequestHandler): """Config request handler.""" #: resource name name = 'config' #: identifier identifier = ('identifier', r'\w+') #: path param path_param = ('path_param', r'\w+') #: allowed HTTP methods allowed_methods = ( 'GET', 'PATCH', ) #: patch mapping patches = { 'anonRedirect': StringField(app, 'ANON_REDIRECT'), 'emby.enabled': BooleanField(app, 'USE_EMBY'), 'torrents.authType': StringField(app, 'TORRENT_AUTH_TYPE'), 'torrents.dir': StringField(app, 'TORRENT_DIR'), 'torrents.enabled': BooleanField(app, 'USE_TORRENTS'), 'torrents.highBandwidth': StringField(app, 'TORRENT_HIGH_BANDWIDTH'), 'torrents.host': StringField(app, 'TORRENT_HOST'), 'torrents.label': StringField(app, 'TORRENT_LABEL'), 'torrents.labelAnime': StringField(app, 'TORRENT_LABEL_ANIME'), 'torrents.method': StringField(app, 'TORRENT_METHOD'), 'torrents.password': StringField(app, 'TORRENT_PASSWORD'), 'torrents.path': BooleanField(app, 'TORRENT_PATH'), 'torrents.paused': BooleanField(app, 'TORRENT_PAUSED'), 'torrents.rpcurl': StringField(app, 'TORRENT_RPCURL'), 'torrents.seedLocation': StringField(app, 'TORRENT_SEED_LOCATION'), 'torrents.seedTime': StringField(app, 'TORRENT_SEED_TIME'), 'torrents.username': StringField(app, 'TORRENT_USERNAME'), 'torrents.verifySSL': BooleanField(app, 'TORRENT_VERIFY_CERT'), 'nzb.enabled': BooleanField(app, 'USE_NZBS'), 'nzb.dir': StringField(app, 'NZB_DIR'), 'nzb.method': StringField(app, 'NZB_METHOD'), 'nzb.nzbget.category': StringField(app, 'NZBGET_CATEGORY'), 'nzb.nzbget.categoryAnime': StringField(app, 'NZBGET_CATEGORY_ANIME'), 'nzb.nzbget.categoryAnimeBacklog': StringField(app, 'NZBGET_CATEGORY_ANIME_BACKLOG'), 'nzb.nzbget.categoryBacklog': StringField(app, 'NZBGET_CATEGORY_BACKLOG'), 'nzb.nzbget.host': StringField(app, 'NZBGET_HOST'), 'nzb.nzbget.password': StringField(app, 'NZBGET_PASSWORD'), 'nzb.nzbget.priority': StringField(app, 'NZBGET_PRIORITY'), 'nzb.nzbget.useHttps': BooleanField(app, 'NZBGET_USE_HTTPS'), 'nzb.nzbget.username': StringField(app, 'NZBGET_USERNAME'), 'nzb.sabnzbd.apiKey': StringField(app, 'SAB_APIKEY'), 'nzb.sabnzbd.category': StringField(app, 'SAB_CATEGORY'), 'nzb.sabnzbd.categoryAnime': StringField(app, 'SAB_CATEGORY_ANIME'), 'nzb.sabnzbd.categoryAnimeBacklog': StringField(app, 'SAB_CATEGORY_ANIME_BACKLOG'), 'nzb.sabnzbd.categoryBacklog': StringField(app, 'SAB_CATEGORY_BACKLOG'), 'nzb.sabnzbd.forced': BooleanField(app, 'SAB_FORCED'), 'nzb.sabnzbd.host': StringField(app, 'SAB_HOST'), 'nzb.sabnzbd.password': StringField(app, 'SAB_PASSWORD'), 'nzb.sabnzbd.username': StringField(app, 'SAB_USERNAME'), 'selectedRootIndex': IntegerField(app, 'SELECTED_ROOT'), 'layout.schedule': EnumField(app, 'COMING_EPS_LAYOUT', ('poster', 'banner', 'list', 'calendar'), default_value='banner', post_processor=layout_schedule_post_processor), 'layout.history': EnumField(app, 'HISTORY_LAYOUT', ('compact', 'detailed'), default_value='detailed'), 'layout.home': EnumField(app, 'HOME_LAYOUT', ('poster', 'small', 'banner', 'simple', 'coverflow'), default_value='poster'), 'layout.show.allSeasons': BooleanField(app, 'DISPLAY_ALL_SEASONS'), 'layout.show.specials': BooleanField(app, 'DISPLAY_SHOW_SPECIALS'), 'layout.show.showListOrder': ListField(app, 'SHOW_LIST_ORDER'), 'theme.name': StringField(app, 'THEME_NAME', setter=theme_name_setter), 'backlogOverview.period': StringField(app, 'BACKLOG_PERIOD'), 'backlogOverview.status': StringField(app, 'BACKLOG_STATUS'), 'rootDirs': ListField(app, 'ROOT_DIRS'), } def get(self, identifier, path_param=None): """Query general configuration. :param identifier: :param path_param: :type path_param: str """ config_sections = DataGenerator.sections() if identifier and identifier not in config_sections: return self._not_found('Config not found') if not identifier: config_data = NonEmptyDict() for section in config_sections: config_data[section] = DataGenerator.get_data(section) return self._ok(data=config_data) config_data = DataGenerator.get_data(identifier) if path_param: if path_param not in config_data: return self._bad_request( '{key} is a invalid path'.format(key=path_param)) config_data = config_data[path_param] return self._ok(data=config_data) def patch(self, identifier, *args, **kwargs): """Patch general configuration.""" if not identifier: return self._bad_request('Config identifier not specified') if identifier != 'main': return self._not_found('Config not found') data = json_decode(self.request.body) accepted = {} ignored = {} for key, value in iter_nested_items(data): patch_field = self.patches.get(key) if patch_field and patch_field.patch(app, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) if ignored: log.warning('Config patch ignored {items!r}', {'items': ignored}) # Make sure to update the config file after everything is updated app.instance.save_config() # Push an update to any open Web UIs through the WebSocket msg = ws.Message('configUpdated', { 'section': identifier, 'config': DataGenerator.get_data(identifier) }) msg.push() self._ok(data=accepted)