diff --git a/changelog.md b/changelog.md index 6ee1ddf..25bdc8b 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,8 @@ ---------- ### 0.2.203 +Added: Properly log or open browser to ipv6 address. See [#553](https://github.com/theotherp/nzbhydra/issues/553). + Changed: Added option to keep "obfuscated" in NzbGeek titles. See [#553](https://github.com/theotherp/nzbhydra/issues/553). Changed: Respect min/max age/size when provided in API calls. This is not specified in the newznab API standards but might be helpful for some. See [#551](https://github.com/theotherp/nzbhydra/issues/551). diff --git a/nzbhydra.py b/nzbhydra.py index 3105303..a460a07 100755 --- a/nzbhydra.py +++ b/nzbhydra.py @@ -171,14 +171,23 @@ def run(arguments): elif config.settings.main.httpProxy: webaccess.set_proxies(config.settings.main.httpProxy, config.settings.main.httpsProxy) - logger.notice("Starting web app on %s:%d" % (host, port)) + if config.settings.main.externalUrl is not None and config.settings.main.externalUrl != "": f = furl(config.settings.main.externalUrl) else: f = furl() - f.host = "127.0.0.1" if config.settings.main.host == "0.0.0.0" else config.settings.main.host + + if config.settings.main.host == "0.0.0.0": + f.host = "127.0.0.1" + elif config.settings.main.host == "::": + f.host = "[::1]" + elif ":" in config.settings.main.host: + f.host = "[%s]" % config.settings.main.host + else: + f.host = config.settings.main.host f.port = port f.scheme = "https" if config.settings.main.ssl else "http" + logger.notice("Starting web app on %s:%d" % (f.host, f.port)) if not arguments.nobrowser and config.settings.main.startupBrowser: if arguments.restarted: logger.info("Not opening the browser after restart") diff --git a/static/js/nzbhydra.js b/static/js/nzbhydra.js index 02001c3..edf7567 100644 --- a/static/js/nzbhydra.js +++ b/static/js/nzbhydra.js @@ -4968,7 +4968,7 @@ function ConfigFields($injector) { type: 'text', label: 'Host', required: true, - placeholder: 'IPv4 address to bind to', + placeholder: 'IPv4/6 address to bind to', help: 'I strongly recommend using a reverse proxy instead of exposing this directly. Requires restart.' }, validators: { diff --git a/static/js/nzbhydra.js.map b/static/js/nzbhydra.js.map index a882d11..744bcbe 100644 --- a/static/js/nzbhydra.js.map +++ b/static/js/nzbhydra.js.map @@ -1 +1 @@ -{"version":3,"sources":["nzbhydra.js","directives/updates.js","directives/title-row.js","directives/title-group.js","directives/tab-or-chart.js","directives/search-result.js","directives/search-result-non-title-columns.js","directives/on-finish-render.js","directives/log.js","directives/keep-focus.js","directives/indexer-input.js","directives/focus-on.js","directives/duplicate-group.js","directives/download-nzbs-button.js","directives/dataTableDirectives.js","directives/connection-test.js","directives/cfg-form-entry.js","directives/backup.js","directives/addable-nzbs.js","directives/addable-nzb.js","update-service.js","update-footer-controller.js","system-controller.js","stats-service.js","stats-controller.js","search-service.js","search-results-controller.js","search-history-service.js","search-history-controller.js","search-controller.js","restart-service.js","nzbhydra-control-service.js","nzb-download-service.js","modal.js","modal-service.js","login-controller.js","indexer-statuses-controller.js","index-controller.js","hydra-auth-service.js","header-controller.js","generic-error-handler.js","formly-config.js","filters.js","downloader-categories-service.js","download-history-controller.js","config-service.js","config-fields-service.js","config-controller.js","categories-service.js","backup-service.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACruBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACbA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjdA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxgEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"nzbhydra.js","sourcesContent":["var nzbhydraapp = angular.module('nzbhydraApp', ['angular-loading-bar', 'cgBusy', 'ui.bootstrap', 'ipCookie', 'angular-growl', 'angular.filter', 'filters', 'ui.router', 'blockUI', 'mgcrea.ngStrap', 'angularUtils.directives.dirPagination', 'nvd3', 'formly', 'formlyBootstrap', 'frapontillo.bootstrap-switch', 'ui.select', 'ngSanitize', 'checklist-model', 'ngAria', 'ngMessages', 'ui.router.title', 'LocalStorageModule', 'angular.filter', 'ngFileUpload', 'ngCookies']);\r\n\r\nangular.module('nzbhydraApp').config([\"$stateProvider\", \"$urlRouterProvider\", \"$locationProvider\", \"blockUIConfig\", \"$urlMatcherFactoryProvider\", \"localStorageServiceProvider\", \"bootstrapped\", function ($stateProvider, $urlRouterProvider, $locationProvider, blockUIConfig, $urlMatcherFactoryProvider, localStorageServiceProvider, bootstrapped) {\r\n\r\n blockUIConfig.autoBlock = false;\r\n $urlMatcherFactoryProvider.strictMode(false);\r\n\r\n $urlRouterProvider.otherwise(\"/\");\r\n\r\n\r\n $stateProvider\r\n .state('root', {\r\n url: '',\r\n abstract: true,\r\n resolve: {\r\n //loginRequired: loginRequired\r\n },\r\n views: {\r\n 'header': {\r\n templateUrl: 'static/html/states/header.html',\r\n controller: 'HeaderController'\r\n },\r\n 'footer': {\r\n templateUrl: 'footer.html'\r\n }\r\n }\r\n })\r\n .state(\"root.config\", {\r\n url: \"/config\",\r\n views: {},\r\n abstract: true\r\n })\r\n .state(\"root.config.main\", {\r\n url: \"/main\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n controllerAs: 'ctrl',\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 0;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Main)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.auth\", {\r\n url: \"/auth\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 1;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Auth)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.searching\", {\r\n url: \"/searching\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 2;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Searching)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.categories\", {\r\n url: \"/categories\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 3;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Categories)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.downloader\", {\r\n url: \"/downloader\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 4;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Downloader)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.indexers\", {\r\n url: \"/indexers\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 5;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Indexers)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats\", {\r\n url: \"/stats\",\r\n abstract: true,\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/stats.html\",\r\n controller: [\"$scope\", \"$state\", function ($scope, $state) {\r\n $scope.$state = $state;\r\n }],\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats\"\r\n }]\r\n }\r\n\r\n }\r\n }\r\n })\r\n .state(\"root.stats.main\", {\r\n url: \"/stats\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: \"static/html/states/main-stats.html\",\r\n controller: \"StatsController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats.indexers\", {\r\n url: \"/indexers\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: \"static/html/states/indexer-statuses.html\",\r\n controller: IndexerStatusesController,\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n statuses: [\"$http\", function ($http) {\r\n return $http.get(\"internalapi/getindexerstatuses\").success(function (response) {\r\n return response.indexerStatuses;\r\n });\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats (Indexers)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats.searches\", {\r\n url: \"/searches\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: \"static/html/states/search-history.html\",\r\n controller: SearchHistoryController,\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n history: ['loginRequired', 'SearchHistoryService', function (loginRequired, SearchHistoryService) {\r\n return SearchHistoryService.getSearchHistory();\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats (Searches)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats.downloads\", {\r\n url: \"/downloads\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: 'static/html/states/download-history.html',\r\n controller: DownloadHistoryController,\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n downloads: [\"StatsService\", function (StatsService) {\r\n return StatsService.getDownloadHistory();\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats (Downloads)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system\", {\r\n url: \"/system\",\r\n views: {},\r\n abstract: true\r\n })\r\n .state(\"root.system.control\", {\r\n url: \"/control\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n askAdmin: ['loginRequired', '$http', function (loginRequired, $http) {\r\n return $http.get(\"internalapi/askadmin\");\r\n }],\r\n activeTab: [function () {\r\n return 0;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.updates\", {\r\n url: \"/updates\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 1;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Updates)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.log\", {\r\n url: \"/log\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 2;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Log)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.backup\", {\r\n url: \"/backup\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 3;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Backup)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.bugreport\", {\r\n url: \"/bugreport\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 4;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Bug report)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.about\", {\r\n url: \"/about\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 5;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (About)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n\r\n .state(\"root.search\", {\r\n url: \"/?category&query&imdbid&tvdbid&title&season&episode&minsize&maxsize&minage&maxage&offsets&rid&mode&tmdbid&indexers\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/search.html\",\r\n controller: \"SearchController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Search\";\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.search.results\", {\r\n views: {\r\n 'results@root.search': {\r\n templateUrl: \"static/html/states/search-results.html\",\r\n controller: \"SearchResultsController\",\r\n controllerAs: \"srController\",\r\n options: {\r\n inherit: true\r\n },\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n var title = \"Search results\";\r\n var details;\r\n if ($stateParams.title) {\r\n details = $stateParams.title;\r\n } else if ($stateParams.query) {\r\n details = $stateParams.query;\r\n }\r\n if (details) {\r\n title += \" (\" + details + \")\";\r\n }\r\n return title;\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.login\", {\r\n url: \"/login\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/login.html\",\r\n controller: \"LoginController\",\r\n resolve: {\r\n loginRequired: function () {\r\n return null;\r\n },\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Login\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n ;\r\n\r\n\r\n $locationProvider.html5Mode(true);\r\n\r\n\r\n function loginRequired($q, $timeout, $state, HydraAuthService, type) {\r\n var deferred = $q.defer();\r\n var userInfos = HydraAuthService.getUserInfos();\r\n var allowed = false;\r\n if (type == \"search\") {\r\n allowed = !userInfos.searchRestricted || userInfos.maySeeSearch;\r\n } else if (type == \"stats\") {\r\n allowed = !userInfos.statsRestricted || userInfos.maySeeStats;\r\n } else if (type == \"admin\") {\r\n allowed = !userInfos.adminRestricted || userInfos.maySeeAdmin;\r\n } else {\r\n allowed = true;\r\n }\r\n if (allowed || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n\r\n //Because I don't know for what state the login is required / asked I have a function for each\r\n\r\n function loginRequiredSearch($q, $timeout, $state, HydraAuthService) {\r\n var deferred = $q.defer();\r\n var userInfos = HydraAuthService.getUserInfos();\r\n if (!userInfos.searchRestricted || userInfos.maySeeSearch || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n function loginRequiredStats($q, $timeout, $state, HydraAuthService) {\r\n var deferred = $q.defer();\r\n\r\n var userInfos = HydraAuthService.getUserInfos();\r\n if (!userInfos.statsRestricted || userInfos.maySeeStats || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n function loginRequiredAdmin($q, $timeout, $state, HydraAuthService) {\r\n var deferred = $q.defer();\r\n\r\n var userInfos = HydraAuthService.getUserInfos();\r\n if (!userInfos.statsRestricted || userInfos.maySeeAdmin || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n localStorageServiceProvider\r\n .setPrefix('nzbhydra');\r\n localStorageServiceProvider\r\n .setNotify(true, false);\r\n}]);\r\n\r\n\r\nnzbhydraapp.config([\"paginationTemplateProvider\", function (paginationTemplateProvider) {\r\n paginationTemplateProvider.setPath('static/html/dirPagination.tpl.html');\r\n}]);\r\n\r\nnzbhydraapp.config(['cfpLoadingBarProvider', function (cfpLoadingBarProvider) {\r\n cfpLoadingBarProvider.latencyThreshold = 100;\r\n}]);\r\n\r\nnzbhydraapp.config(['growlProvider', function (growlProvider) {\r\n growlProvider.globalTimeToLive(5000);\r\n growlProvider.globalPosition('bottom-right');\r\n}]);\r\n\r\nnzbhydraapp.directive('ngEnter', function () {\r\n return function (scope, element, attr) {\r\n element.bind(\"keydown keypress\", function (event) {\r\n if (event.which === 13) {\r\n scope.$apply(function () {\r\n scope.$evalAsync(attr.ngEnter);\r\n });\r\n\r\n event.preventDefault();\r\n }\r\n });\r\n };\r\n});\r\n\r\nnzbhydraapp.filter('nzblink', function () {\r\n return function (resultItem) {\r\n var uri = new URI(\"internalapi/getnzb\");\r\n uri.addQuery(\"searchResultId\", resultItem.searchResultId);\r\n return uri.toString();\r\n }\r\n});\r\n\r\nnzbhydraapp.factory('focus', [\"$rootScope\", \"$timeout\", function ($rootScope, $timeout) {\r\n return function (name) {\r\n $timeout(function () {\r\n $rootScope.$broadcast('focusOn', name);\r\n });\r\n }\r\n}]);\r\n\r\nnzbhydraapp.run([\"$rootScope\", function ($rootScope) {\r\n $rootScope.$on('$stateChangeSuccess',\r\n function (event, toState, toParams, fromState, fromParams) {\r\n try {\r\n $rootScope.title = toState.views[Object.keys(toState.views)[0]].resolve.$title[1](toParams);\r\n } catch (e) {\r\n\r\n }\r\n\r\n });\r\n}]);\r\n\r\n\r\nnzbhydraapp.filter('unsafe', [\"$sce\", function ($sce) {\r\n return $sce.trustAsHtml;\r\n}]);\r\n\r\nnzbhydraapp.filter('dereferer', [\"ConfigService\", function (ConfigService) {\r\n return function (url) {\r\n if (ConfigService.getSafe().dereferer) {\r\n return ConfigService.getSafe().dereferer.replace(\"$s\", escape(url));\r\n }\r\n return url;\r\n }\r\n}]);\r\n\r\nnzbhydraapp.config([\"$provide\", function ($provide) {\r\n $provide.decorator(\"$exceptionHandler\", ['$delegate', '$injector', function ($delegate, $injector) {\r\n return function (exception, cause) {\r\n $delegate(exception, cause);\r\n try {\r\n console.log(exception);\r\n var stack = exception.stack.split('\\n').map(function (line) {\r\n return line.trim();\r\n });\r\n stack = stack.join(\"\\n\");\r\n //$injector.get(\"$http\").put(\"internalapi/logerror\", {error: stack, cause: angular.isDefined(cause) ? cause.toString() : \"No known cause\"});\r\n\r\n\r\n } catch (e) {\r\n console.error(\"Unable to log JS exception to server\", e);\r\n }\r\n };\r\n }]);\r\n}]);\r\n\r\n_.mixin({\r\n isNullOrEmpty: function (string) {\r\n return (_.isUndefined(string) || _.isNull(string) || (_.isString(string) && string.length === 0))\r\n }\r\n});\r\n\r\nnzbhydraapp.factory('sessionInjector', [\"$injector\", function ($injector) {\r\n var sessionInjector = {\r\n response: function (response) {\r\n if (response.headers(\"Hydra-MaySeeAdmin\") != null) {\r\n $injector.get(\"HydraAuthService\").setLoggedInByBasic(response.headers(\"Hydra-MaySeeStats\") == \"True\", response.headers(\"Hydra-MaySeeAdmin\") == \"True\", response.headers(\"Hydra-Username\"))\r\n }\r\n\r\n return response;\r\n }\r\n };\r\n return sessionInjector;\r\n}]);\r\n\r\nnzbhydraapp.config(['$httpProvider', function ($httpProvider) {\r\n $httpProvider.interceptors.push('sessionInjector');\r\n}]);\r\n\r\nnzbhydraapp.directive('autoFocus', [\"$timeout\", function ($timeout) {\r\n return {\r\n restrict: 'AC',\r\n link: function (_scope, _element) {\r\n $timeout(function () {\r\n _element[0].focus();\r\n }, 0);\r\n }\r\n };\r\n}]);\r\n\r\n\r\nnzbhydraapp.factory('focus', [\"$timeout\", \"$window\", function ($timeout, $window) {\r\n return function (id) {\r\n // timeout makes sure that it is invoked after any other event has been triggered.\r\n // e.g. click events that need to run before the focus or\r\n // inputs elements that are in a disabled state but are enabled when those events\r\n // are triggered.\r\n $timeout(function () {\r\n var element = $window.document.getElementById(id);\r\n if (element)\r\n element.focus();\r\n });\r\n };\r\n}]);\r\n\r\nnzbhydraapp.directive('eventFocus', [\"focus\", function (focus) {\r\n return function (scope, elem, attr) {\r\n elem.on(attr.eventFocus, function () {\r\n focus(attr.eventFocusId);\r\n });\r\n\r\n // Removes bound events in the element itself\r\n // when the scope is destroyed\r\n scope.$on('$destroy', function () {\r\n elem.off(attr.eventFocus);\r\n });\r\n };\r\n}]);","angular\r\n .module('nzbhydraApp')\r\n .directive('hydraupdates', hydraupdates);\r\n\r\nfunction hydraupdates() {\r\n controller.$inject = [\"$scope\", \"UpdateService\", \"$sce\"];\r\n return {\r\n templateUrl: 'static/html/directives/updates.html',\r\n controller: controller\r\n };\r\n\r\n function controller($scope, UpdateService, $sce) {\r\n\r\n $scope.loadingPromise = UpdateService.getVersions().then(function (data) {\r\n $scope.currentVersion = data.data.currentVersion;\r\n $scope.repVersion = data.data.repVersion;\r\n $scope.updateAvailable = data.data.updateAvailable;\r\n $scope.changelog = data.data.changelog;\r\n });\r\n \r\n UpdateService.getVersionHistory().then(function(data) {\r\n $scope.versionHistory = $sce.trustAsHtml(data.data.versionHistory);\r\n });\r\n\r\n $scope.update = function () {\r\n UpdateService.update();\r\n };\r\n\r\n $scope.showChangelog = function () {\r\n UpdateService.showChanges($scope.changelog);\r\n };\r\n \r\n \r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('titleRow', titleRow);\r\n\r\nfunction titleRow() {\r\n return {\r\n templateUrl: 'static/html/directives/title-row.html',\r\n scope: {\r\n duplicates: \"<\",\r\n selected: \"<\",\r\n rowIndex: \"@\"\r\n },\r\n controller: ['$scope', '$element', '$attrs', titleRowController]\r\n };\r\n\r\n function titleRowController($scope) {\r\n $scope.expanded = false;\r\n console.log(\"Building title row\");\r\n $scope.duplicatesToShow = duplicatesToShow;\r\n function duplicatesToShow() {\r\n if ($scope.expanded && $scope.duplicates.length > 1) {\r\n console.log(\"Showing all duplicates in group\");\r\n return $scope.duplicates;\r\n } else {\r\n console.log(\"Showing first duplicate in group\");\r\n return [$scope.duplicates[0]];\r\n }\r\n }\r\n\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('titleGroup', titleGroup);\r\n\r\nfunction titleGroup() {\r\n return {\r\n templateUrl: 'static/html/directives/title-group.html',\r\n scope: {\r\n titles: \"<\",\r\n selected: \"=\",\r\n rowIndex: \"<\",\r\n doShowDuplicates: \"<\",\r\n internalRowIndex: \"@\"\r\n },\r\n controller: ['$scope', '$element', '$attrs', controller],\r\n multiElement: true\r\n };\r\n\r\n function controller($scope, $element, $attrs) {\r\n $scope.expanded = false;\r\n $scope.titleGroupExpanded = false;\r\n\r\n $scope.$on(\"toggleTitleExpansion\", function (event, args) {\r\n $scope.titleGroupExpanded = args;\r\n event.stopPropagation();\r\n });\r\n\r\n\r\n $scope.titlesToShow = titlesToShow;\r\n function titlesToShow() {\r\n return $scope.titles.slice(1);\r\n }\r\n \r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('tabOrChart', tabOrChart);\r\n\r\nfunction tabOrChart() {\r\n return {\r\n templateUrl: 'static/html/directives/tab-or-chart.html',\r\n transclude: {\r\n \"chartSlot\": \"chart\",\r\n \"tableSlot\": \"table\"\r\n },\r\n restrict: 'E',\r\n replace: true,\r\n scope: {\r\n display: \"@\"\r\n }\r\n\r\n };\r\n\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('searchResult', searchResult);\r\n\r\nfunction searchResult() {\r\n return {\r\n templateUrl: 'static/html/directives/search-result.html',\r\n require: '^titleGroup',\r\n scope: {\r\n titleGroup: \"<\",\r\n showDuplicates: \"<\",\r\n selected: \"<\",\r\n rowIndex: \"<\"\r\n },\r\n controller: ['$scope', '$element', '$attrs', controller],\r\n multiElement: true\r\n };\r\n\r\n function controller($scope, $element, $attrs) {\r\n $scope.titleGroupExpanded = false;\r\n $scope.hashGroupExpanded = {};\r\n\r\n $scope.toggleTitleGroup = function () {\r\n $scope.titleGroupExpanded = !$scope.titleGroupExpanded;\r\n if (!$scope.titleGroupExpanded) {\r\n $scope.hashGroupExpanded[$scope.titleGroup[0][0].hash] = false; //Also collapse the first title's duplicates\r\n }\r\n };\r\n\r\n $scope.groupingRowDuplicatesToShow = groupingRowDuplicatesToShow;\r\n function groupingRowDuplicatesToShow() {\r\n if ($scope.showDuplicates && $scope.titleGroup[0].length > 1 && $scope.hashGroupExpanded[$scope.titleGroup[0][0].hash]) {\r\n return $scope.titleGroup[0].slice(1);\r\n } else {\r\n return [];\r\n }\r\n }\r\n\r\n //
0 && titleGroupExpanded\" class=\"search-results-row\">\r\n $scope.otherTitleRowsToShow = otherTitleRowsToShow;\r\n function otherTitleRowsToShow() {\r\n if ($scope.titleGroup.length > 1 && $scope.titleGroupExpanded) {\r\n return $scope.titleGroup.slice(1);\r\n } else {\r\n return [];\r\n }\r\n }\r\n \r\n $scope.hashGroupDuplicatesToShow = hashGroupDuplicatesToShow;\r\n function hashGroupDuplicatesToShow(hashGroup) {\r\n if ($scope.showDuplicates && $scope.hashGroupExpanded[hashGroup[0].hash]) {\r\n return hashGroup.slice(1);\r\n } else {\r\n return [];\r\n }\r\n }\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('otherColumns', otherColumns);\r\n\r\nfunction otherColumns($http, $templateCache, $compile, $window) {\r\n controller.$inject = [\"$scope\", \"$http\", \"$uibModal\", \"growl\"];\r\n return {\r\n scope: {\r\n result: \"<\"\r\n },\r\n multiElement: true,\r\n\r\n link: function (scope, element, attrs) {\r\n $http.get('static/html/directives/search-result-non-title-columns.html', {cache: $templateCache}).success(function (templateContent) {\r\n element.replaceWith($compile(templateContent)(scope));\r\n });\r\n\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, $uibModal, growl) {\r\n \r\n $scope.showNfo = showNfo;\r\n function showNfo(resultItem) {\r\n if (resultItem.has_nfo == 0) {\r\n return;\r\n }\r\n var uri = new URI(\"internalapi/getnfo\");\r\n uri.addQuery(\"searchresultid\", resultItem.searchResultId);\r\n return $http.get(uri.toString()).then(function (response) {\r\n if (response.data.has_nfo) {\r\n $scope.openModal(\"lg\", response.data.nfo)\r\n } else {\r\n if (!angular.isUndefined(resultItem.message)) {\r\n growl.error(resultItem.message);\r\n } else {\r\n growl.info(\"No NFO available\");\r\n }\r\n }\r\n });\r\n }\r\n\r\n $scope.openModal = openModal;\r\n\r\n function openModal(size, nfo) {\r\n var modalInstance = $uibModal.open({\r\n template: '
',\r\n controller: NfoModalInstanceCtrl,\r\n size: size,\r\n resolve: {\r\n nfo: function () {\r\n return nfo;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then();\r\n }\r\n \r\n $scope.downloadNzb = downloadNzb;\r\n \r\n function downloadNzb(resultItem) {\r\n //href = \"{{ result.link }}\"\r\n $window.location.href = resultItem.link;\r\n }\r\n\r\n $scope.getNfoTooltip = function() {\r\n if ($scope.result.has_nfo == 1) {\r\n return \"Show NFO\"\r\n } else if ($scope.result.has_nfo == 2) {\r\n return \"Try to load NFO (may not be available)\";\r\n } else {\r\n return \"No NFO available\";\r\n }\r\n }\r\n }\r\n}\r\notherColumns.$inject = [\"$http\", \"$templateCache\", \"$compile\", \"$window\"];\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('NfoModalInstanceCtrl', NfoModalInstanceCtrl);\r\n\r\nfunction NfoModalInstanceCtrl($scope, $uibModalInstance, nfo) {\r\n\r\n $scope.nfo = nfo;\r\n\r\n $scope.ok = function () {\r\n $uibModalInstance.close($scope.selected.item);\r\n };\r\n\r\n $scope.cancel = function () {\r\n $uibModalInstance.dismiss();\r\n };\r\n}\r\nNfoModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"nfo\"];","//Can be used in an ng-repeat directive to call a function when the last element was rendered\r\n//We use it to mark the end of sorting / filtering so we can stop blocking the UI\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .directive('onFinishRender', onFinishRender);\r\n\r\nfunction onFinishRender($timeout) {\r\n function linkFunction(scope, element, attr) {\r\n \r\n if (scope.$last === true) {\r\n $timeout(function () {\r\n scope.$evalAsync(attr.onFinishRender);\r\n });\r\n }\r\n }\r\n\r\n return {\r\n link: linkFunction\r\n }\r\n}\r\nonFinishRender.$inject = [\"$timeout\"];","angular\r\n .module('nzbhydraApp')\r\n .directive('hydralog', hydralog);\r\n\r\nfunction hydralog() {\r\n controller.$inject = [\"$scope\", \"$http\", \"$sce\", \"$interval\", \"localStorageService\"];\r\n return {\r\n templateUrl: \"static/html/directives/log.html\",\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, $sce, $interval, localStorageService) {\r\n $scope.tailInterval = null;\r\n $scope.doUpdateLog = localStorageService.get(\"doUpdateLog\") != null ? localStorageService.get(\"doUpdateLog\") : false;\r\n $scope.doTailLog = localStorageService.get(\"doTailLog\") != null ? localStorageService.get(\"doTailLog\") : false;\r\n\r\n\r\n function getAndShowLog() {\r\n return $http.get(\"internalapi/getlogs\").success(function (data) {\r\n $scope.log = $sce.trustAsHtml(data.log);\r\n });\r\n }\r\n\r\n $scope.logPromise = getAndShowLog();\r\n\r\n $scope.scrollToBottom = function () {\r\n document.getElementById(\"logfile\").scrollTop = 10000000;\r\n document.getElementById(\"logfile\").scrollTop = 100001000;\r\n };\r\n\r\n $scope.update = function () {\r\n getAndShowLog();\r\n $scope.scrollToBottom();\r\n };\r\n\r\n function startUpdateLogInterval() {\r\n $scope.tailInterval = $interval(function () {\r\n getAndShowLog();\r\n if ($scope.doTailLog) {\r\n $scope.scrollToBottom();\r\n }\r\n }, 5000);\r\n }\r\n\r\n $scope.toggleUpdate = function() {\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n } else if ($scope.tailInterval != null) {\r\n console.log(\"Cancelling\");\r\n $interval.cancel($scope.tailInterval);\r\n localStorageService.set(\"doTailLog\", false);\r\n $scope.doTailLog = false;\r\n }\r\n localStorageService.set(\"doUpdateLog\", $scope.doUpdateLog);\r\n };\r\n\r\n $scope.toggleTailLog = function () {\r\n localStorageService.set(\"doTailLog\", $scope.doTailLog);\r\n };\r\n\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n }\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp').directive(\"keepFocus\", ['$timeout', function ($timeout) {\r\n /*\r\n Intended use:\r\n \r\n */\r\n return {\r\n restrict: 'A',\r\n require: 'ngModel',\r\n link: function ($scope, $element, attrs, ngModel) {\r\n\r\n ngModel.$parsers.unshift(function (value) {\r\n $timeout(function () {\r\n $element[0].focus();\r\n });\r\n return value;\r\n });\r\n\r\n }\r\n };\r\n}])","angular\r\n .module('nzbhydraApp')\r\n .directive('indexerInput', indexerInput);\r\n\r\nfunction indexerInput() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-input.html',\r\n scope: {\r\n indexer: \"=\",\r\n model: \"=\",\r\n onClick: \"=\"\r\n },\r\n replace: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.isFocused = false;\r\n \r\n $scope.onFocus = function() {\r\n $scope.isFocused = true;\r\n };\r\n\r\n $scope.onBlur = function () {\r\n $scope.isFocused = false; \r\n };\r\n \r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp').directive('focusOn', focusOn);\r\n\r\nfunction focusOn() {\r\n return directive;\r\n function directive(scope, elem, attr) {\r\n scope.$on('focusOn', function (e, name) {\r\n if (name === attr.focusOn) {\r\n elem[0].focus();\r\n }\r\n });\r\n }\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('duplicateGroup', duplicateGroup);\r\n\r\nfunction duplicateGroup() {\r\n titleRowController.$inject = [\"$scope\", \"localStorageService\"];\r\n return {\r\n templateUrl: 'static/html/directives/duplicate-group.html',\r\n scope: {\r\n duplicates: \"<\",\r\n selected: \"=\",\r\n isFirstRow: \"<\",\r\n rowIndex: \"<\",\r\n displayTitleToggle: \"<\",\r\n internalRowIndex: \"@\"\r\n },\r\n controller: titleRowController\r\n };\r\n\r\n function titleRowController($scope, localStorageService) {\r\n $scope.internalRowIndex = Number($scope.internalRowIndex);\r\n $scope.rowIndex = Number($scope.rowIndex);\r\n $scope.titlesExpanded = false;\r\n $scope.duplicatesExpanded = false;\r\n $scope.foo = {\r\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") != null ? localStorageService.get(\"duplicatesDisplayed\") : false\r\n };\r\n $scope.duplicatesToShow = duplicatesToShow;\r\n function duplicatesToShow() {\r\n return $scope.duplicates.slice(1);\r\n }\r\n\r\n $scope.toggleTitleExpansion = function () {\r\n $scope.titlesExpanded = !$scope.titlesExpanded;\r\n $scope.$emit(\"toggleTitleExpansion\", $scope.titlesExpanded);\r\n };\r\n\r\n $scope.toggleDuplicateExpansion = function () {\r\n $scope.duplicatesExpanded = !$scope.duplicatesExpanded;\r\n };\r\n\r\n $scope.$on(\"invertSelection\", function () {\r\n for (var i = 0; i < $scope.duplicates.length; i++) {\r\n if ($scope.duplicatesExpanded) {\r\n invertSelection($scope.selected, $scope.duplicates[i]);\r\n } else {\r\n if (i > 0) {\r\n //Always remove duplicates that aren't displayed\r\n invertSelection($scope.selected, $scope.duplicates[i], true);\r\n } else {\r\n invertSelection($scope.selected, $scope.duplicates[i]);\r\n }\r\n }\r\n }\r\n });\r\n\r\n $scope.$on(\"duplicatesDisplayed\", function (event, args) {\r\n $scope.foo.duplicatesDisplayed = args;\r\n });\r\n\r\n $scope.clickCheckbox = function (event) {\r\n var globalCheckboxIndex = $scope.rowIndex * 1000 + $scope.internalRowIndex * 100 + Number(event.currentTarget.dataset.checkboxIndex);\r\n console.log(globalCheckboxIndex);\r\n $scope.$emit(\"checkboxClicked\", event, globalCheckboxIndex, event.currentTarget.checked);\r\n };\r\n\r\n function isBetween(num, betweena, betweenb) {\r\n return (betweena <= num && num <= betweenb) || (betweena >= num && num >= betweenb);\r\n }\r\n\r\n $scope.$on(\"shiftClick\", function (event, startIndex, endIndex, newValue) {\r\n var globalDuplicateGroupIndex = $scope.rowIndex * 1000 + $scope.internalRowIndex * 100;\r\n if (isBetween(globalDuplicateGroupIndex, startIndex, endIndex)) {\r\n\r\n for (var i = 0; i < $scope.duplicates.length; i++) {\r\n if (isBetween(globalDuplicateGroupIndex + i, startIndex, endIndex)) {\r\n if (i == 0 || $scope.duplicatesExpanded) {\r\n console.log(\"Indirectly clicked row with global index \" + (globalDuplicateGroupIndex + i) + \" setting new checkbox value to \" + newValue);\r\n var index = _.indexOf($scope.selected, $scope.duplicates[i]);\r\n if (index == -1 && newValue) {\r\n console.log(\"Adding to selection\");\r\n $scope.selected.push($scope.duplicates[i]);\r\n } else if (index > -1 && !newValue) {\r\n $scope.selected.splice(index, 1);\r\n console.log(\"Removing from selection\");\r\n }\r\n }\r\n }\r\n }\r\n }\r\n });\r\n\r\n function invertSelection(a, b, dontPush) {\r\n var index = _.indexOf(a, b);\r\n if (index > -1) {\r\n a.splice(index, 1);\r\n } else {\r\n if (!dontPush)\r\n a.push(b);\r\n }\r\n }\r\n }\r\n\r\n\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('downloadNzbsButton', downloadNzbsButton);\r\n\r\nfunction downloadNzbsButton() {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/download-nzbs-button.html',\r\n require: ['^searchResults'],\r\n scope: {\r\n searchResults: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService, growl) {\r\n\r\n $scope.downloaders = NzbDownloadService.getEnabledDownloaders();\r\n\r\n $scope.download = function (downloader) {\r\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length == 0) {\r\n growl.info(\"You should select at least one result...\");\r\n } else {\r\n\r\n var values = _.map($scope.searchResults, function (value) {\r\n return value.searchResultId;\r\n });\r\n\r\n NzbDownloadService.download(downloader, values).then(function (response) {\r\n if (response.data.success) {\r\n growl.info(\"Successfully added \" + response.data.added + \" of \" + response.data.of + \" NZBs\");\r\n } else {\r\n growl.error(\"Error while adding NZBs\");\r\n }\r\n }, function () {\r\n growl.error(\"Error while adding NZBs\");\r\n });\r\n }\r\n }\r\n\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp').directive(\"columnFilterWrapper\", columnFilterWrapper);\r\n\r\nfunction columnFilterWrapper() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: 'static/html/dataTable/columnFilterOuter.html',\r\n transclude: true,\r\n controllerAs: 'columnFilterWrapperCtrl',\r\n scope: true,\r\n bindToController: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n var vm = this;\r\n\r\n vm.open = false;\r\n vm.isActive = false;\r\n\r\n vm.toggle = function () {\r\n vm.open = !vm.open;\r\n if (vm.open) {\r\n $scope.$broadcast(\"opened\");\r\n }\r\n };\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\r\n vm.open = false;\r\n vm.isActive = isActive;\r\n })\r\n }\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"freetextFilter\", freetextFilter);\r\n\r\nfunction freetextFilter() {\r\n controller.$inject = [\"$scope\", \"focus\"];\r\n return {\r\n template: '',\r\n require: \"^columnFilterWrapper\",\r\n controllerAs: 'innerController',\r\n scope: {\r\n column: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, focus) {\r\n $scope.data = {};\r\n\r\n $scope.$on(\"opened\", function () {\r\n focus(\"freetext-filter-input\");\r\n });\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n $scope.$emit(\"filter\", $scope.column, {filter: $scope.data.filter, filtertype: \"freetext\"}, angular.isDefined($scope.data.filter) && $scope.data.filter.length > 0);\r\n }\r\n }\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"checkboxesFilter\", checkboxesFilter);\r\n\r\nfunction checkboxesFilter() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n controllerAs: 'checkboxesFilterController',\r\n scope: {\r\n column: \"@\",\r\n entries: \"<\",\r\n preselect: \"<\",\r\n showInvert: \"<\",\r\n isBoolean: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.selected = {\r\n entries: []\r\n };\r\n\r\n if ($scope.preselect) {\r\n $scope.selected.entries = $scope.entries.slice();\r\n }\r\n\r\n $scope.invert = function () {\r\n $scope.selected.entries = _.difference($scope.entries, $scope.selected.entries);\r\n };\r\n\r\n $scope.apply = function () {\r\n console.log($scope.selected);\r\n var isActive = $scope.selected.entries.length < $scope.entries.length;\r\n $scope.$emit(\"filter\", $scope.column, {filter: _.pluck($scope.selected.entries, \"id\"), filtertype: \"checkboxes\", isBoolean: $scope.isBoolean}, isActive)\r\n }\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"booleanFilter\", booleanFilter);\r\n\r\nfunction booleanFilter() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n controllerAs: 'booleanFilterController',\r\n scope: {\r\n column: \"@\",\r\n options: \"<\",\r\n preselect: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n\r\n function controller($scope) {\r\n $scope.selected = {value: $scope.options[$scope.preselect].value};\r\n\r\n $scope.apply = function () {\r\n console.log($scope.selected);\r\n $scope.$emit(\"filter\", $scope.column, {filter: $scope.selected.value, filtertype: \"boolean\"}, $scope.selected.value != $scope.options[0].value)\r\n }\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"timeFilter\", timeFilter);\r\n\r\nfunction timeFilter() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n scope: {\r\n column: \"@\",\r\n selected: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n $scope.apply = function () {\r\n var isActive = $scope.selected.beforeDate || $scope.selected.afterDate;\r\n $scope.$emit(\"filter\", $scope.column, {filter: {after: $scope.selected.afterDate, before: $scope.selected.beforeDate}, filtertype: \"time\"}, isActive)\r\n }\r\n }\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"columnSortable\", columnSortable);\r\n\r\nfunction columnSortable() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: \"static/html/dataTable/columnSortable.html\",\r\n transclude: true,\r\n scope: {\r\n sortMode: \"@\", //0: no sorting, 1: asc, 2: desc\r\n column: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n if (angular.isUndefined($scope.sortMode)) {\r\n $scope.sortMode = 0;\r\n }\r\n\r\n $scope.$on(\"newSortColumn\", function(event, column) {\r\n if (column != $scope.column) {\r\n $scope.sortMode = 0;\r\n }\r\n });\r\n\r\n $scope.sort = function () {\r\n $scope.sortMode = ($scope.sortMode + 1) % 3;\r\n $scope.$emit(\"sort\", $scope.column, $scope.sortMode)\r\n };\r\n\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('connectionTest', connectionTest);\r\n\r\nfunction connectionTest() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/connection-test.html',\r\n require: ['^type', '^data'],\r\n scope: {\r\n type: \"=\",\r\n id: \"=\",\r\n data: \"=\",\r\n downloader: \"=\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.message = \"\";\r\n console.log($scope);\r\n\r\n var testButton = \"#button-test-connection\";\r\n var testMessage = \"#message-test-connection\";\r\n\r\n function showSuccess() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-danger\");\r\n angular.element(testButton).addClass(\"btn-success\");\r\n }\r\n\r\n function showError() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-success\");\r\n angular.element(testButton).addClass(\"btn-danger\");\r\n }\r\n\r\n $scope.testConnection = function () {\r\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\r\n var myInjector = angular.injector([\"ng\"]);\r\n var $http = myInjector.get(\"$http\");\r\n var url;\r\n var params;\r\n if ($scope.type == \"downloader\") {\r\n url = \"internalapi/test_downloader\";\r\n params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};\r\n if ($scope.downloader == \"sabnzbd\") {\r\n params.apikey = $scope.data.apikey;\r\n params.url = $scope.data.url;\r\n } else {\r\n params.host = $scope.data.host;\r\n params.port = $scope.data.port;\r\n params.ssl = $scope.data.ssl;\r\n }\r\n } else if ($scope.data.type == \"newznab\") {\r\n url = \"internalapi/test_newznab\";\r\n params = {host: $scope.data.host, apikey: $scope.data.apikey};\r\n if (angular.isDefined($scope.data.username)) {\r\n params[\"username\"] = $scope.data.username;\r\n params[\"password\"] = $scope.data.password;\r\n }\r\n }\r\n $http.get(url, {params: params}).success(function (result) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \r\n if (result.result) {\r\n angular.element(testMessage).text(\"\");\r\n showSuccess();\r\n } else {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }\r\n\r\n }).error(function () {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }).finally(function () {\r\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\r\n })\r\n }\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('cfgFormEntry', cfgFormEntry);\r\n\r\nfunction cfgFormEntry() {\r\n return {\r\n templateUrl: 'static/html/directives/cfg-form-entry.html',\r\n require: [\"^title\", \"^cfg\"],\r\n scope: {\r\n title: \"@\",\r\n cfg: \"=\",\r\n help: \"@\",\r\n type: \"@?\",\r\n options: \"=?\"\r\n },\r\n controller: [\"$scope\", \"$element\", \"$attrs\", function ($scope, $element, $attrs) {\r\n $scope.type = angular.isDefined($scope.type) ? $scope.type : 'text';\r\n $scope.options = angular.isDefined($scope.type) ? $scope.$eval($attrs.options) : [];\r\n }]\r\n };\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('hydrabackup', hydrabackup);\r\n\r\nfunction hydrabackup() {\r\n controller.$inject = [\"$scope\", \"BackupService\", \"Upload\", \"RequestsErrorHandler\", \"growl\", \"RestartService\", \"$http\"];\r\n return {\r\n templateUrl: 'static/html/directives/backup.html',\r\n controller: controller\r\n };\r\n\r\n function controller($scope, BackupService, Upload, RequestsErrorHandler, growl, RestartService, $http) {\r\n $scope.refreshBackupList = function () {\r\n BackupService.getBackupsList().then(function (backups) {\r\n $scope.backups = backups;\r\n });\r\n };\r\n\r\n $scope.refreshBackupList();\r\n\r\n $scope.uploadActive = false;\r\n\r\n\r\n $scope.createAndDownloadBackupFile = function() {\r\n\r\n $http({method: 'GET', url: 'internalapi/getbackup', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {\r\n var a = document.createElement('a');\r\n var blob = new Blob([data], {'type': \"application/octet-stream\"});\r\n a.href = URL.createObjectURL(blob);\r\n a.download = \"nzbhydra-backup-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\r\n\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n $scope.refreshBackupList();\r\n }).error(function (data, status, headers, config) {\r\n console.log(\"Error:\" + status);\r\n });\r\n\r\n };\r\n\r\n $scope.uploadBackupFile = function (file, errFiles) {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n console.log(\"Hallo\");\r\n $scope.file = file;\r\n $scope.errFile = errFiles && errFiles[0];\r\n if (file) {\r\n $scope.uploadActive = true;\r\n file.upload = Upload.upload({\r\n url: 'internalapi/restorebackup',\r\n data: {content: file}\r\n });\r\n\r\n file.upload.then(function (response) {\r\n $scope.uploadActive = false;\r\n file.result = response.data;\r\n RestartService.restart(\"Restore successful.\");\r\n\r\n }, function (response) {\r\n $scope.uploadActive = false;\r\n growl.error(response.data)\r\n }, function (evt) {\r\n file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));\r\n file.loaded = Math.floor(evt.loaded / 1024);\r\n file.total = Math.floor(evt.total / 1024);\r\n });\r\n }\r\n });\r\n };\r\n\r\n $scope.restoreFromFile = function(filename) {\r\n BackupService.restoreFromFile(filename).then(function() {\r\n RestartService.restart(\"Restore successful.\");\r\n },\r\n function(response) {\r\n growl.error(response.data);\r\n })\r\n }\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('addableNzbs', addableNzbs);\r\n\r\nfunction addableNzbs() {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\"];\r\n return {\r\n templateUrl: 'static/html/directives/addable-nzbs.html',\r\n require: ['^searchResultId'],\r\n scope: {\r\n searchResultId: \"<\",\r\n downloadType: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService) {\r\n $scope.downloaders = _.filter(NzbDownloadService.getEnabledDownloaders(), function(downloader) {\r\n if ($scope.downloadType != \"nzb\") {\r\n return downloader.downloadType == $scope.downloadType\r\n }\r\n return true;\r\n });\r\n }\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('addableNzb', addableNzb);\r\n\r\nfunction addableNzb() {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/addable-nzb.html',\r\n scope: {\r\n searchResultId: \"<\",\r\n downloader: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService, growl) {\r\n if ($scope.downloader.iconCssClass) {\r\n $scope.cssClass = \"fa fa-\" + $scope.downloader.iconCssClass.replace(\"fa-\",\"\").replace(\"fa \", \"\"); \r\n } else {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd\" : \"nzbget\";\r\n }\r\n \r\n $scope.add = function () {\r\n $scope.cssClass = \"nzb-spinning\";\r\n NzbDownloadService.download($scope.downloader, [$scope.searchResultId]).then(function (response) {\r\n if (response.data.success) {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-success\" : \"nzbget-success\";\r\n } else {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(\"Unable to add NZB. Make sure the downloader is running and properly configured.\");\r\n }\r\n }, function () {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(\"An unexpected error occurred while trying to contact NZB Hydra or add the NZB.\");\r\n })\r\n };\r\n \r\n \r\n\r\n }\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('UpdateService', UpdateService);\r\n\r\nfunction UpdateService($http, growl, blockUI, RestartService) {\r\n\r\n var currentVersion;\r\n var repVersion;\r\n var updateAvailable;\r\n var changelog;\r\n var versionHistory;\r\n \r\n return {\r\n update: update,\r\n showChanges: showChanges,\r\n getVersions: getVersions,\r\n getChangelog: getChangelog,\r\n getVersionHistory: getVersionHistory\r\n };\r\n \r\n \r\n \r\n function getVersions() {\r\n return $http.get(\"internalapi/get_versions\").then(function (data) {\r\n currentVersion = data.data.currentVersion;\r\n repVersion = data.data.repVersion;\r\n updateAvailable = data.data.updateAvailable;\r\n return data;\r\n });\r\n }\r\n\r\n function getChangelog() {\r\n return $http.get(\"internalapi/get_changelog\", {currentVersion: currentVersion, repVersion: repVersion}).then(function (data) {\r\n changelog = data.data.changelog;\r\n return data;\r\n });\r\n }\r\n \r\n function getVersionHistory() {\r\n return $http.get(\"internalapi/get_version_history\").then(function (data) {\r\n versionHistory = data.data.versionHistory;\r\n return data;\r\n });\r\n }\r\n\r\n function showChanges(changelog) {\r\n\r\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\r\n var $uibModal = myInjector.get(\"$uibModal\");\r\n var params = {\r\n size: \"lg\",\r\n templateUrl: \"static/html/changelog.html\",\r\n resolve: {\r\n changelog: function () {\r\n return changelog;\r\n }\r\n },\r\n controller: function ($scope, $sce, $uibModalInstance, changelog) {\r\n //I fucking hate that untrusted HTML shit\r\n changelog = $sce.trustAsHtml(changelog);\r\n $scope.changelog = changelog;\r\n console.log(changelog);\r\n $scope.ok = function () {\r\n $uibModalInstance.dismiss();\r\n };\r\n }\r\n };\r\n\r\n var modalInstance = $uibModal.open(params);\r\n\r\n modalInstance.result.then();\r\n }\r\n \r\n\r\n function update() {\r\n blockUI.start(\"Updating. Please stand by...\");\r\n $http.get(\"internalapi/update\").then(function (data) {\r\n if (data.data.success) {\r\n RestartService.restart(\"Update complete.\", 15);\r\n } else {\r\n blockUI.reset();\r\n growl.info(\"An error occurred while updating. Please check the logs.\");\r\n }\r\n },\r\n function () {\r\n blockUI.reset();\r\n growl.info(\"An error occurred while updating. Please check the logs.\");\r\n });\r\n }\r\n}\r\nUpdateService.$inject = [\"$http\", \"growl\", \"blockUI\", \"RestartService\"];\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('UpdateFooterController', UpdateFooterController);\r\n\r\nfunction UpdateFooterController($scope, UpdateService, HydraAuthService) {\r\n\r\n $scope.updateAvailable = false;\r\n $scope.checked = false;\r\n\r\n $scope.mayUpdate = HydraAuthService.getUserInfos().maySeeAdmin;\r\n\r\n $scope.$on(\"user:loggedIn\", function () {\r\n if (HydraAuthService.getUserInfos().maySeeAdmin && !$scope.checked) {\r\n retrieveUpdateInfos();\r\n }\r\n });\r\n\r\n\r\n if ($scope.mayUpdate) {\r\n retrieveUpdateInfos();\r\n }\r\n\r\n function retrieveUpdateInfos() {\r\n $scope.checked = true;\r\n UpdateService.getVersions().then(function (data) {\r\n $scope.currentVersion = data.data.currentVersion;\r\n $scope.repVersion = data.data.repVersion;\r\n $scope.updateAvailable = data.data.updateAvailable;\r\n $scope.changelog = data.data.changelog;\r\n });\r\n }\r\n\r\n\r\n $scope.update = function () {\r\n UpdateService.update();\r\n };\r\n\r\n $scope.showChangelog = function () {\r\n UpdateService.showChanges($scope.changelog);\r\n }\r\n\r\n}\r\nUpdateFooterController.$inject = [\"$scope\", \"UpdateService\", \"HydraAuthService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('SystemController', SystemController);\r\n\r\nfunction SystemController($scope, $state, activeTab, $http, growl, RestartService, ModalService, UpdateService, NzbHydraControlService) {\r\n\r\n $scope.activeTab = activeTab;\r\n\r\n $scope.shutdown = function () {\r\n NzbHydraControlService.shutdown().then(function () {\r\n growl.info(\"Shutdown initiated. Cya!\");\r\n },\r\n function () {\r\n growl.info(\"Unable to send shutdown command.\");\r\n })\r\n };\r\n\r\n $scope.restart = function () {\r\n RestartService.restart();\r\n };\r\n\r\n $scope.deleteLogAndDatabase = function () {\r\n ModalService.open(\"Delete log and db\", \"Are you absolutely sure you want to delete your database and log files? Hydra will restart to do that.\", {\r\n yes: {\r\n onYes: function () {\r\n NzbHydraControlService.deleteLogAndDb();\r\n RestartService.countdown();\r\n },\r\n text: \"Yes, delete log and database\"\r\n },\r\n no: {\r\n onCancel: function () {\r\n\r\n },\r\n text: \"Nah\"\r\n }\r\n });\r\n };\r\n\r\n $scope.forceUpdate = function() {\r\n UpdateService.update()\r\n };\r\n \r\n\r\n $scope.allTabs = [\r\n {\r\n active: false,\r\n state: 'root.system.control',\r\n name: \"Control\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.updates',\r\n name: \"Updates\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.log',\r\n name: \"Log\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.backup',\r\n name: \"Backup\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.bugreport',\r\n name: \"Bugreport\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.about',\r\n name: \"About\"\r\n }\r\n ];\r\n\r\n\r\n $scope.goToSystemState = function (index) {\r\n $state.go($scope.allTabs[index].state, {activeTab: index}, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.downloadDebuggingInfos = function() {\r\n $http({method: 'GET', url: 'internalapi/getdebugginginfos', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {\r\n var a = document.createElement('a');\r\n var blob = new Blob([data], {'type': \"application/octet-stream\"});\r\n a.href = URL.createObjectURL(blob);\r\n var filename = \"nzbhydra-debuginfo-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\r\n a.download = filename;\r\n \r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n }).error(function (data, status, headers, config) {\r\n console.log(\"Error:\" + status);\r\n });\r\n }\r\n \r\n}\r\nSystemController.$inject = [\"$scope\", \"$state\", \"activeTab\", \"$http\", \"growl\", \"RestartService\", \"ModalService\", \"UpdateService\", \"NzbHydraControlService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('StatsService', StatsService);\r\n\r\nfunction StatsService($http) {\r\n\r\n return {\r\n get: getStats,\r\n getDownloadHistory: getDownloadHistory\r\n };\r\n\r\n function getStats(after, before) {\r\n return $http.get(\"internalapi/getstats\", {params: {after:after, before:before}}).success(function (response) {\r\n return response.data;\r\n });\r\n }\r\n\r\n function getDownloadHistory(pageNumber, limit, filterModel, sortModel) {\r\n var params = {page: pageNumber, limit: limit, filterModel: filterModel};\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n }\r\n return $http.post(\"internalapi/getnzbdownloads\", params).success(function (response) {\r\n return {\r\n nzbDownloads: response.nzbDownloads,\r\n totalDownloads: response.totalDownloads\r\n };\r\n \r\n });\r\n }\r\n\r\n}\r\nStatsService.$inject = [\"$http\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('StatsController', StatsController);\r\n\r\nfunction StatsController($scope, $filter, StatsService, blockUI) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n var initializingAfter = true;\r\n var initializingBefore = true;\r\n $scope.afterDate = moment().subtract(30, \"days\").toDate();\r\n $scope.beforeDate = moment().toDate();\r\n updateStats();\r\n\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n function updateStats() {\r\n blockUI.start(\"Updating stats...\");\r\n var after = $scope.afterDate != null ? Math.floor($scope.afterDate.getTime() / 1000) : null;\r\n var before = $scope.beforeDate != null ? Math.floor($scope.beforeDate.getTime() / 1000) : null;\r\n StatsService.get(after, before).then(function(stats) {\r\n $scope.setStats(stats);\r\n });\r\n\r\n blockUI.reset();\r\n }\r\n\r\n $scope.$watch('beforeDate', function () {\r\n if (initializingBefore) {\r\n initializingBefore = false;\r\n } else {\r\n updateStats();\r\n }\r\n });\r\n\r\n\r\n $scope.$watch('afterDate', function () {\r\n if (initializingAfter) {\r\n initializingAfter = false;\r\n } else {\r\n updateStats();\r\n }\r\n });\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n updateStats();\r\n }\r\n };\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n\r\n $scope.setStats = function (stats) {\r\n stats = stats.data;\r\n\r\n $scope.nzbDownloads = null;\r\n $scope.avgResponseTimes = stats.avgResponseTimes;\r\n $scope.avgIndexerSearchResultsShares = stats.avgIndexerSearchResultsShares;\r\n $scope.avgIndexerAccessSuccesses = stats.avgIndexerAccessSuccesses;\r\n $scope.indexerDownloadShares = stats.indexerDownloadShares;\r\n $scope.downloadsPerHourOfDay = stats.timeBasedDownloadStats.perHourOfDay;\r\n $scope.downloadsPerDayOfWeek = stats.timeBasedDownloadStats.perDayOfWeek;\r\n $scope.searchesPerHourOfDay = stats.timeBasedSearchStats.perHourOfDay;\r\n $scope.searchesPerDayOfWeek = stats.timeBasedSearchStats.perDayOfWeek;\r\n\r\n\r\n var numIndexers = $scope.avgResponseTimes.length;\r\n\r\n $scope.avgResponseTimesChart = getChart(\"multiBarHorizontalChart\", $scope.avgResponseTimes, \"name\", \"avgResponseTime\", \"\", \"Response time\");\r\n $scope.avgResponseTimesChart.options.chart.margin.left = 100;\r\n $scope.avgResponseTimesChart.options.chart.yAxis.rotateLabels = -30;\r\n var avgResponseTimesChartHeight = Math.max(numIndexers * 30, 350);\r\n $scope.avgResponseTimesChart.options.chart.height = avgResponseTimesChartHeight;\r\n\r\n $scope.resultsSharesChart = getResultsSharesChart();\r\n\r\n var rotation = 30;\r\n if (numIndexers > 30) {\r\n rotation = 70;\r\n }\r\n $scope.resultsSharesChart.options.chart.xAxis.rotateLabels = rotation;\r\n $scope.resultsSharesChart.options.chart.height = avgResponseTimesChartHeight;\r\n\r\n $scope.downloadsPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.downloadsPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Downloads');\r\n $scope.downloadsPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.downloadsPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.downloadsPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Downloads');\r\n $scope.downloadsPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.searchesPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.searchesPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Searches');\r\n $scope.searchesPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.searchesPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.searchesPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Searches');\r\n $scope.searchesPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.indexerDownloadSharesChart = {\r\n options: {\r\n chart: {\r\n type: 'pieChart',\r\n height: 500,\r\n x: function (d) {\r\n return d.name;\r\n },\r\n y: function (d) {\r\n return d.share;\r\n },\r\n showLabels: true,\r\n duration: 500,\r\n labelThreshold: 0.01,\r\n labelSunbeamLayout: true,\r\n tooltip: {\r\n valueFormatter: function (d, i) {\r\n return $filter('number')(d, 2) + \"%\";\r\n }\r\n },\r\n legend: {\r\n margin: {\r\n top: 5,\r\n right: 35,\r\n bottom: 5,\r\n left: 0\r\n }\r\n }\r\n }\r\n },\r\n data: $scope.indexerDownloadShares\r\n };\r\n\r\n $scope.indexerDownloadSharesChart.options.chart.height = Math.min(Math.max(numIndexers * 40, 350), 900);\r\n };\r\n\r\n\r\n function getChart(chartType, values, xKey, yKey, xAxisLabel, yAxisLabel) {\r\n return {\r\n options: {\r\n chart: {\r\n type: chartType,\r\n height: 350,\r\n margin: {\r\n top: 20,\r\n right: 20,\r\n bottom: 100,\r\n left: 50\r\n },\r\n x: function (d) {\r\n return d[xKey];\r\n },\r\n y: function (d) {\r\n return d[yKey];\r\n },\r\n showValues: true,\r\n valueFormat: function (d) {\r\n return d;\r\n },\r\n color: function () {\r\n return \"red\"\r\n },\r\n showControls: false,\r\n showLegend: false,\r\n duration: 100,\r\n xAxis: {\r\n axisLabel: xAxisLabel,\r\n tickFormat: function (d) {\r\n return d;\r\n },\r\n rotateLabels: 30,\r\n showMaxMin: false,\r\n color: function () {\r\n return \"white\"\r\n }\r\n },\r\n yAxis: {\r\n axisLabel: yAxisLabel,\r\n axisLabelDistance: -10,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n },\r\n tooltip: {\r\n enabled: false\r\n },\r\n zoom: {\r\n enabled: true,\r\n scaleExtent: [1, 10],\r\n useFixedDomain: false,\r\n useNiceScale: false,\r\n horizontalOff: false,\r\n verticalOff: true,\r\n unzoomEventType: 'dblclick.zoom'\r\n }\r\n }\r\n }, data: [{\r\n \"key\": \"doesntmatter\",\r\n \"bar\": true,\r\n \"values\": values\r\n }]\r\n };\r\n }\r\n\r\n //Was unable to use the function above for this and gave up\r\n function getResultsSharesChart() {\r\n return {\r\n options: {\r\n chart: {\r\n type: 'multiBarChart',\r\n height: 350,\r\n margin: {\r\n top: 20,\r\n right: 20,\r\n bottom: 100,\r\n left: 45\r\n },\r\n\r\n clipEdge: true,\r\n duration: 500,\r\n stacked: false,\r\n reduceXTicks: false,\r\n showValues: true,\r\n tooltip: {\r\n enabled: true,\r\n valueFormatter: function (d) {\r\n return d + \"%\";\r\n }\r\n },\r\n showControls: false,\r\n xAxis: {\r\n axisLabel: '',\r\n showMaxMin: false,\r\n rotateLabels: 30,\r\n axisLabelDistance: 30,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n },\r\n yAxis: {\r\n axisLabel: 'Share (%)',\r\n axisLabelDistance: -20,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n }\r\n }\r\n },\r\n\r\n data: [\r\n {\r\n key: \"Results\",\r\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\r\n return {series: 0, y: stats.avgResultsShare, x: stats.name}\r\n })\r\n },\r\n {\r\n key: \"Unique results\",\r\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\r\n return {series: 1, y: stats.avgUniqueResults, x: stats.name}\r\n })\r\n }\r\n ]\r\n };\r\n }\r\n\r\n\r\n}\r\nStatsController.$inject = [\"$scope\", \"$filter\", \"StatsService\", \"blockUI\"];\r\n","//\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('SearchService', SearchService);\r\n\r\nfunction SearchService($http) {\r\n\r\n\r\n var lastExecutedQuery;\r\n var lastResults;\r\n\r\n return {\r\n search: search,\r\n getLastResults: getLastResults,\r\n loadMore: loadMore\r\n };\r\n \r\n\r\n function search(category, query, tmdbid, imdbid, title, tvdbid, rid, season, episode, minsize, maxsize, minage, maxage, indexers, mode) {\r\n var uri;\r\n if (category.indexOf(\"Movies\") > -1 || (category.indexOf(\"20\") == 0) || mode == \"movie\") {\r\n uri = new URI(\"internalapi/moviesearch\");\r\n if (angular.isDefined(tmdbid)) {\r\n uri.addQuery(\"tmdbid\", tmdbid);\r\n } else if (angular.isDefined(imdbid)) {\r\n uri.addQuery(\"imdbid\", imdbid);\r\n } else {\r\n uri.addQuery(\"query\", query);\r\n }\r\n\r\n } else if (category.indexOf(\"TV\") > -1 || (category.indexOf(\"50\") == 0) || mode == \"tvsearch\") {\r\n uri = new URI(\"internalapi/tvsearch\");\r\n if (angular.isDefined(tvdbid)) {\r\n uri.addQuery(\"tvdbid\", tvdbid);\r\n }\r\n if (angular.isDefined(rid)) {\r\n uri.addQuery(\"rid\", rid);\r\n } else {\r\n uri.addQuery(\"query\", query);\r\n }\r\n\r\n if (angular.isDefined(season)) {\r\n uri.addQuery(\"season\", season);\r\n }\r\n if (angular.isDefined(episode)) {\r\n uri.addQuery(\"episode\", episode);\r\n }\r\n } else {\r\n uri = new URI(\"internalapi/search\");\r\n uri.addQuery(\"query\", query);\r\n }\r\n if (angular.isDefined(title)) {\r\n uri.addQuery(\"title\", title);\r\n }\r\n if (_.isNumber(minsize)) {\r\n uri.addQuery(\"minsize\", minsize);\r\n }\r\n if (_.isNumber(maxsize)) {\r\n uri.addQuery(\"maxsize\", maxsize);\r\n }\r\n if (_.isNumber(minage)) {\r\n uri.addQuery(\"minage\", minage);\r\n }\r\n if (_.isNumber(maxage)) {\r\n uri.addQuery(\"maxage\", maxage);\r\n }\r\n if (!angular.isUndefined(indexers)) {\r\n uri.addQuery(\"indexers\", decodeURIComponent(indexers));\r\n }\r\n \r\n\r\n uri.addQuery(\"category\", category);\r\n lastExecutedQuery = uri;\r\n return $http.get(uri.toString()).then(processData);\r\n\r\n }\r\n\r\n function loadMore(offset, loadAll) {\r\n lastExecutedQuery.removeQuery(\"offset\");\r\n lastExecutedQuery.addQuery(\"offset\", offset);\r\n lastExecutedQuery.addQuery(\"loadAll\", loadAll ? true : false);\r\n\r\n return $http.get(lastExecutedQuery.toString()).then(processData);\r\n }\r\n\r\n function processData(response) {\r\n var results = response.data.results;\r\n var indexersearches = response.data.indexersearches;\r\n var total = response.data.total;\r\n var rejected = response.data.rejected;\r\n var resultsCount = results.length;\r\n\r\n\r\n //Sum up response times of indexers from individual api accesses\r\n //TODO: Move this to search result controller because we need to update it every time we loaded more results\r\n _.each(indexersearches, function (ps) {\r\n if (ps.did_search) {\r\n ps.averageResponseTime = _.reduce(ps.apiAccesses, function (memo, rp) {\r\n return memo + rp.response_time;\r\n }, 0);\r\n ps.averageResponseTime = ps.averageResponseTime / ps.apiAccesses.length;\r\n }\r\n });\r\n \r\n lastResults = {\"results\": results, \"indexersearches\": indexersearches, \"total\": total, \"resultsCount\": resultsCount, \"rejected\": rejected};\r\n return lastResults;\r\n }\r\n \r\n function getLastResults() {\r\n return lastResults;\r\n }\r\n}\r\nSearchService.$inject = [\"$http\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('SearchResultsController', SearchResultsController);\r\n\r\nfunction sumRejected(rejected) {\r\n return _.reduce(rejected, function (memo, entry) {\r\n return memo + entry[1];\r\n }, 0);\r\n}\r\n\r\n//SearchResultsController.$inject = ['blockUi'];\r\nfunction SearchResultsController($stateParams, $scope, $q, $timeout, blockUI, growl, localStorageService, SearchService, ConfigService) {\r\n\r\n if (localStorageService.get(\"sorting\") != null) {\r\n var sorting = localStorageService.get(\"sorting\");\r\n $scope.sortPredicate = sorting.predicate;\r\n $scope.sortReversed = sorting.reversed;\r\n } else {\r\n $scope.sortPredicate = \"epoch\";\r\n $scope.sortReversed = true;\r\n }\r\n $scope.limitTo = 100;\r\n $scope.offset = 0;\r\n //Handle incoming data\r\n\r\n $scope.indexersearches = _.sortBy(SearchService.getLastResults().indexersearches, function (i) {\r\n return i.indexer.toLowerCase()\r\n });\r\n $scope.indexerDisplayState = []; //Stores if a indexer's results should be displayed or not\r\n $scope.indexerResultsInfo = {}; //Stores information about the indexer's results like how many we already retrieved\r\n $scope.groupExpanded = {};\r\n $scope.selected = [];\r\n $scope.lastClicked = null;\r\n $scope.lastClickedValue = null;\r\n\r\n $scope.foo = {\r\n indexerStatusesExpanded: localStorageService.get(\"indexerStatusesExpanded\") != null ? localStorageService.get(\"indexerStatusesExpanded\") : false,\r\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") != null ? localStorageService.get(\"duplicatesDisplayed\") : false\r\n };\r\n\r\n $scope.countFilteredOut = 0;\r\n\r\n //Initially set visibility of all found indexers to true, they're needed for initial filtering / sorting\r\n _.forEach($scope.indexersearches, function (ps) {\r\n $scope.indexerDisplayState[ps.indexer.toLowerCase()] = true;\r\n });\r\n\r\n _.forEach($scope.indexersearches, function (ps) {\r\n $scope.indexerResultsInfo[ps.indexer.toLowerCase()] = {loadedResults: ps.loaded_results};\r\n });\r\n\r\n //Process results\r\n $scope.results = SearchService.getLastResults().results;\r\n $scope.total = SearchService.getLastResults().total;\r\n $scope.resultsCount = SearchService.getLastResults().resultsCount;\r\n $scope.rejected = SearchService.getLastResults().rejected;\r\n $scope.countRejected = sumRejected($scope.rejected);\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n\r\n $scope.$emit(\"searchResultsShown\");\r\n stopBlocking();\r\n\r\n //Returns the content of the property (defined by the current sortPredicate) of the first group element \r\n $scope.firstResultPredicate = firstResultPredicate;\r\n function firstResultPredicate(item) {\r\n return item[0][$scope.sortPredicate];\r\n }\r\n\r\n //Returns the unique group identifier which allows angular to keep track of the grouped search results even after filtering, making filtering by indexers a lot faster (albeit still somewhat slow...) \r\n $scope.groupId = groupId;\r\n function groupId(item) {\r\n return item[0][0].searchResultId;\r\n }\r\n\r\n //Block the UI and return after timeout. This way we make sure that the blocking is done before angular starts updating the model/view. There's probably a better way to achieve that?\r\n function startBlocking(message) {\r\n var deferred = $q.defer();\r\n blockUI.start(message);\r\n $timeout(function () {\r\n deferred.resolve();\r\n }, 100);\r\n return deferred.promise;\r\n }\r\n\r\n //Set sorting according to the predicate. If it's the same as the old one, reverse, if not sort by the given default (so that age is descending, name ascending, etc.)\r\n //Sorting (and filtering) are really slow (about 2 seconds for 1000 results from 5 indexers) but I haven't found any way of making it faster, apart from the tracking \r\n $scope.setSorting = setSorting;\r\n function setSorting(predicate, reversedDefault) {\r\n if (predicate == $scope.sortPredicate) {\r\n $scope.sortReversed = !$scope.sortReversed;\r\n } else {\r\n $scope.sortReversed = reversedDefault;\r\n }\r\n $scope.sortPredicate = predicate;\r\n startBlocking(\"Sorting / filtering...\").then(function () {\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n blockUI.reset();\r\n localStorageService.set(\"sorting\", {predicate: predicate, reversed: $scope.sortReversed});\r\n });\r\n }\r\n\r\n $scope.inlineFilter = inlineFilter;\r\n function inlineFilter(result) {\r\n var ok = true;\r\n ok = ok && $scope.titleFilter && result.title.toLowerCase().indexOf($scope.titleFilter) > -1;\r\n ok = ok && $scope.minSizeFilter && $scope.minSizeFilter * 1024 * 1024 < result.size;\r\n ok = ok && $scope.maxSizeFilter && $scope.maxSizeFilter * 1024 * 1024 > result.size;\r\n return ok;\r\n }\r\n\r\n\r\n $scope.$on(\"searchInputChanged\", function (event, query, minage, maxage, minsize, maxsize) {\r\n $scope.filteredResults = sortAndFilter($scope.results, query, minage, maxage, minsize, maxsize);\r\n });\r\n\r\n $scope.resort = function () {\r\n };\r\n\r\n function sortAndFilter(results, query, minage, maxage, minsize, maxsize) {\r\n $scope.countFilteredOut = 0;\r\n\r\n function filterByAgeAndSize(item) {\r\n var ok = true;\r\n ok = ok && (!_.isNumber(minsize) || item.size / 1024 / 1024 >= minsize)\r\n && (!_.isNumber(maxsize) || item.size / 1024 / 1024 <= maxsize)\r\n && (!_.isNumber(minage) || item.age_days >= Number(minage))\r\n && (!_.isNumber(maxage) || item.age_days <= Number(maxage));\r\n\r\n if (ok && query) {\r\n var words = query.toLowerCase().split(\" \");\r\n ok = _.every(words, function (word) {\r\n return item.title.toLowerCase().indexOf(word) > -1;\r\n });\r\n }\r\n if (!ok) {\r\n $scope.countFilteredOut++;\r\n }\r\n return ok;\r\n }\r\n\r\n\r\n function getItemIndexerDisplayState(item) {\r\n return $scope.indexerDisplayState[item.indexer.toLowerCase()];\r\n }\r\n\r\n function getCleanedTitle(element) {\r\n return element.title.toLowerCase().replace(/[\\s\\-\\._]/ig, \"\");\r\n }\r\n\r\n function createSortedHashgroups(titleGroup) {\r\n\r\n function createHashGroup(hashGroup) {\r\n //Sorting hash group's contents should not matter for size and age and title but might for category (we might remove this, it's probably mostly unnecessary)\r\n var sortedHashGroup = _.sortBy(hashGroup, function (item) {\r\n var sortPredicateValue;\r\n if ($scope.sortPredicate == \"grabs\") {\r\n sortPredicateValue = angular.isDefined(item.grabs) ? item.grabs : 0;\r\n } else {\r\n sortPredicateValue = item[$scope.sortPredicate];\r\n }\r\n //var sortPredicateValue = item[$scope.sortPredicate];\r\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\r\n });\r\n //Now sort the hash group by indexer score (inverted) so that the result with the highest indexer score is shown on top (or as the only one of a hash group if it's collapsed)\r\n sortedHashGroup = _.sortBy(sortedHashGroup, function (item) {\r\n return item.indexerscore * -1;\r\n });\r\n return sortedHashGroup;\r\n }\r\n\r\n function getHashGroupFirstElementSortPredicate(hashGroup) {\r\n if ($scope.sortPredicate == \"grabs\") {\r\n sortPredicateValue = angular.isDefined(hashGroup[0].grabs) ? hashGroup[0].grabs : 0;\r\n } else {\r\n var sortPredicateValue = hashGroup[0][$scope.sortPredicate];\r\n }\r\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\r\n }\r\n\r\n return _.chain(titleGroup).groupBy(\"hash\").map(createHashGroup).sortBy(getHashGroupFirstElementSortPredicate).value();\r\n }\r\n\r\n function getTitleGroupFirstElementsSortPredicate(titleGroup) {\r\n var sortPredicateValue;\r\n if ($scope.sortPredicate == \"title\") {\r\n sortPredicateValue = titleGroup[0][0].title.toLowerCase();\r\n } else if ($scope.sortPredicate == \"grabs\") {\r\n sortPredicateValue = angular.isDefined(titleGroup[0][0].grabs) ? titleGroup[0][0].grabs : 0;\r\n } else {\r\n sortPredicateValue = titleGroup[0][0][$scope.sortPredicate];\r\n }\r\n\r\n return sortPredicateValue;\r\n }\r\n\r\n var filtered = _.chain(results)\r\n //Filter by age, size and title\r\n .filter(filterByAgeAndSize)\r\n //Remove elements of which the indexer is currently hidden \r\n .filter(getItemIndexerDisplayState)\r\n //Make groups of results with the same title \r\n .groupBy(getCleanedTitle)\r\n //For every title group make subgroups of duplicates and sort the group \r\n .map(createSortedHashgroups)\r\n //And then sort the title group using its first hashgroup's first item (the group itself is already sorted and so are the hash groups) \r\n .sortBy(getTitleGroupFirstElementsSortPredicate)\r\n .value();\r\n if ($scope.sortReversed) {\r\n filtered = filtered.reverse();\r\n }\r\n if ($scope.countFilteredOut > 0) {\r\n growl.info(\"Filtered \" + $scope.countFilteredOut + \" of the retrieved results\");\r\n }\r\n\r\n $scope.lastClicked = null;\r\n return filtered;\r\n }\r\n\r\n $scope.toggleTitlegroupExpand = function toggleTitlegroupExpand(titleGroup) {\r\n $scope.groupExpanded[titleGroup[0][0].title] = !$scope.groupExpanded[titleGroup[0][0].title];\r\n $scope.groupExpanded[titleGroup[0][0].hash] = !$scope.groupExpanded[titleGroup[0][0].hash];\r\n };\r\n\r\n\r\n $scope.stopBlocking = stopBlocking;\r\n function stopBlocking() {\r\n blockUI.reset();\r\n }\r\n\r\n $scope.loadMore = loadMore;\r\n function loadMore(loadAll) {\r\n startBlocking(loadAll ? \"Loading all results...\" : \"Loading more results...\").then(function () {\r\n SearchService.loadMore($scope.resultsCount, loadAll).then(function (data) {\r\n $scope.results = $scope.results.concat(data.results);\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n $scope.total = data.total;\r\n $scope.rejected = data.rejected;\r\n $scope.countRejected = sumRejected($scope.rejected);\r\n $scope.resultsCount += data.resultsCount;\r\n stopBlocking();\r\n });\r\n });\r\n }\r\n\r\n\r\n//Filters the results according to new visibility settings.\r\n $scope.toggleIndexerDisplay = toggleIndexerDisplay;\r\n function toggleIndexerDisplay(indexer) {\r\n $scope.indexerDisplayState[indexer.toLowerCase()] = $scope.indexerDisplayState[indexer.toLowerCase()];\r\n startBlocking(\"Filtering. Sorry...\").then(function () {\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n }).then(function () {\r\n stopBlocking();\r\n });\r\n }\r\n\r\n $scope.countResults = countResults;\r\n function countResults() {\r\n return $scope.results.length;\r\n }\r\n\r\n $scope.invertSelection = function invertSelection() {\r\n $scope.$broadcast(\"invertSelection\");\r\n };\r\n\r\n $scope.toggleIndexerStatuses = function () {\r\n $scope.foo.indexerStatusesExpanded = !$scope.foo.indexerStatusesExpanded;\r\n localStorageService.set(\"indexerStatusesExpanded\", $scope.foo.indexerStatusesExpanded);\r\n };\r\n\r\n $scope.toggleDuplicatesDisplayed = function () {\r\n //$scope.foo.duplicatesDisplayed = !$scope.foo.duplicatesDisplayed;\r\n localStorageService.set(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\r\n $scope.$broadcast(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\r\n };\r\n\r\n $scope.$on(\"checkboxClicked\", function (event, originalEvent, rowIndex, newCheckedValue) {\r\n if (originalEvent.shiftKey && $scope.lastClicked != null) {\r\n $scope.$broadcast(\"shiftClick\", Number($scope.lastClicked), Number(rowIndex), Number($scope.lastClickedValue));\r\n }\r\n $scope.lastClicked = rowIndex;\r\n $scope.lastClickedValue = newCheckedValue;\r\n })\r\n\r\n $scope.filterRejectedZero = function() {\r\n return function (entry) {\r\n return entry[1] > 0;\r\n }\r\n }\r\n}\r\nSearchResultsController.$inject = [\"$stateParams\", \"$scope\", \"$q\", \"$timeout\", \"blockUI\", \"growl\", \"localStorageService\", \"SearchService\", \"ConfigService\"];\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('SearchHistoryService', SearchHistoryService);\r\n\r\nfunction SearchHistoryService($filter, $http) {\r\n\r\n return {\r\n getSearchHistory: getSearchHistory,\r\n getSearchHistoryForSearching: getSearchHistoryForSearching,\r\n formatRequest: formatRequest,\r\n getStateParamsForRepeatedSearch: getStateParamsForRepeatedSearch\r\n };\r\n\r\n function getSearchHistoryForSearching() {\r\n return $http.post(\"internalapi/getsearchrequestsforsearching\").success(function (response) {\r\n return {\r\n searchRequests: response.searchRequests,\r\n totalRequests: response.totalRequests\r\n }\r\n });\r\n }\r\n\r\n function getSearchHistory(pageNumber, limit, filterModel, sortModel, distinct, onlyCurrentUser) {\r\n var params = {\r\n page: pageNumber,\r\n limit: limit,\r\n filterModel: filterModel,\r\n distinct: distinct,\r\n onlyCurrentUser: onlyCurrentUser\r\n };\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n }\r\n return $http.post(\"internalapi/getsearchrequests\", params).success(function (response) {\r\n return {\r\n searchRequests: response.searchRequests,\r\n totalRequests: response.totalRequests\r\n }\r\n });\r\n }\r\n\r\n function formatRequest(request, includeIdLink, includequery, describeEmptySearch, includeTitle) {\r\n var result = [];\r\n //ID key: ID value\r\n //season\r\n //episode\r\n //author\r\n //title\r\n if (includequery && request.query) {\r\n result.push(\"Query: \" + request.query);\r\n }\r\n if (request.title && includeTitle) {\r\n result.push('Title: ' + request.title);\r\n } else if (request.movietitle && includeTitle) {\r\n result.push('Title: ' + request.movietitle);\r\n } else if (request.tvtitle && includeTitle) {\r\n result.push('Title: ' + request.tvtitle);\r\n } else if (request.identifier_key) {\r\n var href;\r\n var key;\r\n if (request.identifier_key == \"imdbid\") {\r\n key = \"IMDB ID\";\r\n href = \"https://www.imdb.com/title/tt\"\r\n } else if (request.identifier_key == \"tvdbid\") {\r\n key = \"TVDB ID\";\r\n href = \"https://thetvdb.com/?tab=series&id=\"\r\n } else if (request.identifier_key == \"rid\") {\r\n key = \"TVRage ID\";\r\n href = \"internalapi/redirect_rid?rid=\"\r\n } else if (request.identifier_key == \"tmdb\") {\r\n key = \"TMDV ID\";\r\n href = \"https://www.themoviedb.org/movie/\"\r\n }\r\n href = href + request.identifier_value;\r\n href = $filter(\"dereferer\")(href);\r\n if (includeIdLink) {\r\n result.push('' + key + ': ' + request.identifier_value + \"\");\r\n } else {\r\n result.push('' + key + \": \" + request.identifier_value);\r\n }\r\n }\r\n if (request.season) {\r\n result.push('Season: ' + request.season);\r\n }\r\n if (request.episode) {\r\n result.push('Episode: ' + request.episode);\r\n }\r\n if (request.author) {\r\n result.push('Author: ' + request.author);\r\n }\r\n if (result.length == 0 && describeEmptySearch) {\r\n result = ['Empty search'];\r\n }\r\n\r\n return result.join(\", \");\r\n\r\n }\r\n\r\n function getStateParamsForRepeatedSearch(request) {\r\n var stateParams = {};\r\n stateParams.mode = \"search\"\r\n if (request.identifier_key == \"imdbid\") {\r\n stateParams.mode = \"movie\"\r\n stateParams.imdbid = request.identifier_value;\r\n } else if (request.identifier_key == \"tvdbid\" || request.identifier_key == \"rid\") {\r\n stateParams.mode = \"tvsearch\";\r\n if (request.identifier_key == \"rid\") {\r\n stateParams.rid = request.identifier_value;\r\n } else {\r\n stateParams.tvdbid = request.identifier_value;\r\n }\r\n\r\n if (request.season != \"\") {\r\n stateParams.season = request.season;\r\n }\r\n if (request.episode != \"\") {\r\n stateParams.episode = request.episode;\r\n }\r\n }\r\n if (request.query != \"\") {\r\n stateParams.query = request.query;\r\n }\r\n\r\n\r\n if (request.movietitle != null) {\r\n stateParams.title = request.movietitle;\r\n }\r\n if (request.tvtitle != null) {\r\n stateParams.title = request.tvtitle;\r\n }\r\n\r\n if (request.category) {\r\n stateParams.category = request.category;\r\n }\r\n\r\n stateParams.category = request.category;\r\n\r\n return stateParams;\r\n }\r\n\r\n\r\n}\r\nSearchHistoryService.$inject = [\"$filter\", \"$http\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('SearchHistoryController', SearchHistoryController);\r\n\r\n\r\nfunction SearchHistoryController($scope, $state, SearchHistoryService, ConfigService, history, $sce, $filter) {\r\n $scope.limit = 100;\r\n $scope.pagination = {\r\n current: 1\r\n };\r\n $scope.sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n $scope.filterModel = {};\r\n\r\n //Filter options\r\n $scope.categoriesForFiltering = [];\r\n _.forEach(ConfigService.getSafe().categories, function (category) {\r\n $scope.categoriesForFiltering.push({label: category.pretty, id: category.pretty})\r\n });\r\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\r\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: false}, {label: \"Internal\", value: true}];\r\n\r\n //Preloaded data\r\n $scope.searchRequests = history.data.searchRequests;\r\n $scope.totalRequests = history.data.totalRequests;\r\n\r\n $scope.update = function () {\r\n SearchHistoryService.getSearchHistory($scope.pagination.current, $scope.limit, $scope.filterModel, $scope.sortModel).then(function (history) {\r\n $scope.searchRequests = history.data.searchRequests;\r\n $scope.totalRequests = history.data.totalRequests;\r\n });\r\n };\r\n\r\n $scope.$on(\"sort\", function (event, column, sortMode) {\r\n if (sortMode == 0) {\r\n column = \"time\";\r\n sortMode = 2;\r\n }\r\n $scope.sortModel = {\r\n column: column,\r\n sortMode: sortMode\r\n };\r\n $scope.$broadcast(\"newSortColumn\", column);\r\n $scope.update();\r\n });\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\r\n if (filterModel.filter) {\r\n $scope.filterModel[column] = filterModel;\r\n } else {\r\n delete $scope.filterModel[column];\r\n }\r\n $scope.update();\r\n });\r\n\r\n\r\n $scope.openSearch = function (request) {\r\n var stateParams = {};\r\n if (request.identifier_key == \"imdbid\") {\r\n stateParams.imdbid = request.identifier_value;\r\n } else if (request.identifier_key == \"tvdbid\" || request.identifier_key == \"rid\") {\r\n if (request.identifier_key == \"rid\") {\r\n stateParams.rid = request.identifier_value;\r\n } else {\r\n stateParams.tvdbid = request.identifier_value;\r\n }\r\n\r\n if (request.season != \"\") {\r\n stateParams.season = request.season;\r\n }\r\n if (request.episode != \"\") {\r\n stateParams.episode = request.episode;\r\n }\r\n }\r\n if (request.query != \"\") {\r\n stateParams.query = request.query;\r\n }\r\n if (request.type == \"tv\") {\r\n stateParams.mode = \"tvsearch\"\r\n } else if (request.type == \"movie\") {\r\n stateParams.mode = \"movie\"\r\n } else {\r\n stateParams.mode = \"search\"\r\n }\r\n\r\n if (request.movietitle != null) {\r\n stateParams.title = request.movietitle;\r\n }\r\n if (request.tvtitle != null) {\r\n stateParams.title = request.tvtitle;\r\n }\r\n\r\n if (request.category) {\r\n stateParams.category = request.category;\r\n }\r\n\r\n stateParams.category = request.category;\r\n\r\n $state.go(\"root.search\", stateParams, {inherit: false});\r\n };\r\n\r\n $scope.formatQuery = function (request) {\r\n if (request.movietitle != null) {\r\n return request.movietitle;\r\n }\r\n if (request.tvtitle != null) {\r\n return request.tvtitle;\r\n }\r\n\r\n if (!request.query && !request.identifier_key && !request.season && !request.episode) {\r\n return \"Update query\";\r\n }\r\n return request.query;\r\n };\r\n\r\n $scope.formatAdditional = function (request) {\r\n var result = [];\r\n //ID key: ID value\r\n //season\r\n //episode\r\n //author\r\n //title\r\n if (request.identifier_key) {\r\n var href;\r\n var key;\r\n if (request.identifier_key == \"imdbid\") {\r\n key = \"IMDB ID\";\r\n href = \"https://www.imdb.com/title/tt\"\r\n } else if (request.identifier_key == \"tvdbid\") {\r\n key = \"TVDB ID\";\r\n href = \"https://thetvdb.com/?tab=series&id=\"\r\n } else if (request.identifier_key == \"rid\") {\r\n key = \"TVRage ID\";\r\n href = \"internalapi/redirect_rid?rid=\"\r\n } else if (request.identifier_key == \"tmdb\") {\r\n key = \"TMDV ID\";\r\n href = \"https://www.themoviedb.org/movie/\"\r\n }\r\n href = href + request.identifier_value;\r\n href = $filter(\"dereferer\")(href);\r\n result.push(key + \": \" + '' + request.identifier_value + \"\");\r\n }\r\n if (request.season) {\r\n result.push(\"Season: \" + request.season);\r\n }\r\n if (request.episode) {\r\n result.push(\"Episode: \" + request.episode);\r\n }\r\n if (request.author) {\r\n result.push(\"Author: \" + request.author);\r\n }\r\n if (request.title) {\r\n result.push(\"Title: \" + request.title);\r\n }\r\n return $sce.trustAsHtml(result.join(\", \"));\r\n };\r\n\r\n\r\n\r\n\r\n}\r\nSearchHistoryController.$inject = [\"$scope\", \"$state\", \"SearchHistoryService\", \"ConfigService\", \"history\", \"$sce\", \"$filter\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('SearchController', SearchController);\r\n\r\nfunction SearchController($scope, $http, $stateParams, $state, $window, $filter, $sce, growl, SearchService, focus, ConfigService, CategoriesService, blockUI, $element, ModalService, SearchHistoryService) {\r\n\r\n function getNumberOrUndefined(number) {\r\n if (_.isUndefined(number) || _.isNaN(number) || number == \"\") {\r\n return undefined;\r\n }\r\n number = parseInt(number);\r\n if (_.isNumber(number)) {\r\n return number;\r\n } else {\r\n return undefined;\r\n }\r\n }\r\n\r\n //Fill the form with the search values we got from the state params (so that their values are the same as in the current url)\r\n $scope.mode = $stateParams.mode;\r\n $scope.categories = _.filter(CategoriesService.getAll(), function (c) {\r\n return c.mayBeSelected && c.ignoreResults != \"internal\" && c.ignoreResults != \"always\";\r\n });\r\n if (angular.isDefined($stateParams.category) && $stateParams.category) {\r\n $scope.category = CategoriesService.getByName($stateParams.category);\r\n } else {\r\n $scope.category = CategoriesService.getDefault();\r\n }\r\n $scope.category = (_.isUndefined($stateParams.category) || $stateParams.category == \"\") ? CategoriesService.getDefault() : CategoriesService.getByName($stateParams.category);\r\n $scope.tmdbid = $stateParams.tmdbid;\r\n $scope.tvdbid = $stateParams.tvdbid;\r\n $scope.imdbid = $stateParams.imdbid;\r\n $scope.rid = $stateParams.rid;\r\n $scope.title = $stateParams.title;\r\n $scope.season = $stateParams.season;\r\n $scope.episode = $stateParams.episode;\r\n $scope.query = $stateParams.query;\r\n $scope.minsize = getNumberOrUndefined($stateParams.minsize);\r\n $scope.maxsize = getNumberOrUndefined($stateParams.maxsize);\r\n $scope.minage = getNumberOrUndefined($stateParams.minage);\r\n $scope.maxage = getNumberOrUndefined($stateParams.maxage);\r\n if (!_.isUndefined($scope.title) && _.isUndefined($scope.query)) {\r\n //$scope.query = $scope.title;\r\n }\r\n if (!angular.isUndefined($stateParams.indexers)) {\r\n $scope.indexers = decodeURIComponent($stateParams.indexers).split(\"|\");\r\n }\r\n\r\n $scope.showIndexers = {};\r\n\r\n $scope.searchHistory = [];\r\n\r\n var safeConfig = ConfigService.getSafe();\r\n\r\n //Doesn't belong here but whatever\r\n var firstStartThreeDaysAgo = ConfigService.getSafe().firstStart < moment().subtract(3, \"days\").unix();\r\n var doShowSurvey = (ConfigService.getSafe().pollShown == 0 && firstStartThreeDaysAgo) || ConfigService.getSafe().pollShown == 1;\r\n if (doShowSurvey) {\r\n var message;\r\n if (ConfigService.getSafe().pollShown == 0) {\r\n message = \"Dear user, I would like to ask you to answer a short query about NZB Hydra. It is absolutely anonymous and will not take more than a couple of minutes. You would help me a lot!\";\r\n } else {\r\n message = \"Dear user, thank you for answering my last survey. Unfortunately I'm an idiot and didn't know that SurveyMonkey would only show me the first 100 results. Please be so kind and answer the new survey :-)\";\r\n }\r\n ModalService.open(\"User query\",\r\n message, {\r\n yes: {\r\n onYes: function () {\r\n $window.open($filter(\"dereferer\")(\"https://goo.gl/forms/F3PwtEor2krBxLcR2\"), \"_blank\");\r\n $http.get(\"internalapi/pollshown\", {params: {selection: 1}});\r\n ConfigService.getSafe().pollShown = 2;\r\n },\r\n text: \"Yes, I want to help. Take me there.\"\r\n },\r\n cancel: {\r\n onCancel: function () {\r\n $http.get(\"internalapi/pollshown\", {params: {selection: 0}});\r\n ConfigService.getSafe().pollShown = 0;\r\n },\r\n text: \"Not now. Remind me.\"\r\n },\r\n no: {\r\n onNo: function () {\r\n $http.get(\"internalapi/pollshown\", {params: {selection: -1}});\r\n ConfigService.getSafe().pollShown = -1;\r\n },\r\n text: \"Nah, feck off!\"\r\n }\r\n });\r\n }\r\n\r\n\r\n $scope.typeAheadWait = 300;\r\n $scope.selectedItem = \"\";\r\n $scope.autocompleteLoading = false;\r\n $scope.isAskById = $scope.category.supportsById;\r\n $scope.isById = {value: true}; //If true the user wants to search by id so we enable autosearch. Was unable to achieve this using a simple boolean\r\n $scope.availableIndexers = [];\r\n $scope.autocompleteClass = \"autocompletePosterMovies\";\r\n\r\n $scope.toggle = function (searchCategory) {\r\n $scope.category = searchCategory;\r\n\r\n //Show checkbox to ask if the user wants to search by ID (using autocomplete)\r\n $scope.isAskById = $scope.category.supportsById;\r\n\r\n focus('searchfield');\r\n\r\n //Hacky way of triggering the autocomplete loading\r\n var searchModel = $element.find(\"#searchfield\").controller(\"ngModel\");\r\n if (angular.isDefined(searchModel.$viewValue)) {\r\n searchModel.$setViewValue(searchModel.$viewValue + \" \");\r\n }\r\n\r\n if (safeConfig.searching.enableCategorySizes) {\r\n var min = searchCategory.min;\r\n var max = searchCategory.max;\r\n if (_.isNumber(min)) {\r\n $scope.minsize = min;\r\n } else {\r\n $scope.minsize = \"\";\r\n }\r\n if (_.isNumber(max)) {\r\n $scope.maxsize = max;\r\n } else {\r\n $scope.maxsize = \"\";\r\n }\r\n }\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n\r\n\r\n };\r\n\r\n\r\n // Any function returning a promise object can be used to load values asynchronously\r\n $scope.getAutocomplete = function (val) {\r\n $scope.autocompleteLoading = true;\r\n //Expected model returned from API:\r\n //label: What to show in the results\r\n //title: Will be used for file search\r\n //value: Will be used as extraInfo (ttid oder tvdb id)\r\n //poster: url of poster to show\r\n\r\n //Don't use autocomplete if checkbox is disabled\r\n if (!$scope.isById.value) {\r\n return {};\r\n }\r\n\r\n if ($scope.category.name.indexOf(\"movies\") > -1) {\r\n return $http.get('internalapi/autocomplete?type=movie', {\r\n params: {\r\n input: val\r\n }\r\n }).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data.results;\r\n });\r\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\r\n\r\n return $http.get('internalapi/autocomplete?type=tv', {\r\n params: {\r\n input: val\r\n }\r\n }).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data.results;\r\n });\r\n } else {\r\n return {};\r\n }\r\n };\r\n\r\n\r\n $scope.startSearch = function () {\r\n blockUI.start(\"Searching...\");\r\n var indexers = angular.isUndefined($scope.indexers) ? undefined : $scope.indexers.join(\"|\");\r\n SearchService.search($scope.category.name, $scope.query, $scope.tmdbid, $scope.imdbid, $scope.title, $scope.tvdbid, $scope.rid, $scope.season, $scope.episode, $scope.minsize, $scope.maxsize, $scope.minage, $scope.maxage, indexers, $scope.mode).then(function () {\r\n $state.go(\"root.search.results\", {\r\n minsize: $scope.minsize,\r\n maxsize: $scope.maxsize,\r\n minage: $scope.minage,\r\n maxage: $scope.maxage\r\n }, {\r\n inherit: true\r\n });\r\n $scope.tmdbid = undefined;\r\n $scope.imdbid = undefined;\r\n $scope.tvdbid = undefined;\r\n });\r\n };\r\n\r\n function getSelectedIndexers() {\r\n var activatedIndexers = _.filter($scope.availableIndexers).filter(function (indexer) {\r\n return indexer.activated;\r\n });\r\n return _.pluck(activatedIndexers, \"name\").join(\"|\");\r\n }\r\n\r\n\r\n $scope.goToSearchUrl = function () {\r\n var stateParams = {};\r\n if ($scope.category.name.indexOf(\"movies\") > -1) {\r\n stateParams.title = $scope.title;\r\n stateParams.mode = \"movie\";\r\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\r\n stateParams.mode = \"tvsearch\";\r\n stateParams.title = $scope.title;\r\n } else if ($scope.category.name == \"ebook\") {\r\n stateParams.mode = \"ebook\";\r\n } else {\r\n stateParams.mode = \"search\";\r\n }\r\n\r\n stateParams.tmdbid = $scope.tmdbid;\r\n stateParams.tvdbid = $scope.tvdbid;\r\n stateParams.title = $scope.title;\r\n stateParams.season = $scope.season;\r\n stateParams.episode = $scope.episode;\r\n stateParams.query = $scope.query;\r\n stateParams.minsize = $scope.minsize;\r\n stateParams.maxsize = $scope.maxsize;\r\n stateParams.minage = $scope.minage;\r\n stateParams.maxage = $scope.maxage;\r\n stateParams.category = $scope.category.name;\r\n stateParams.indexers = encodeURIComponent(getSelectedIndexers());\r\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.repeatSearch = function (request) {\r\n $state.go(\"root.search\", SearchHistoryService.getStateParamsForRepeatedSearch(request), {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n\r\n $scope.selectAutocompleteItem = function ($item) {\r\n $scope.selectedItem = $item;\r\n $scope.title = $item.title;\r\n if ($scope.category.name.indexOf(\"movies\") > -1) {\r\n $scope.tmdbid = $item.value;\r\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\r\n $scope.tvdbid = $item.value;\r\n }\r\n $scope.query = \"\";\r\n $scope.goToSearchUrl();\r\n };\r\n\r\n $scope.startQuerySearch = function () {\r\n if (!$scope.query) {\r\n growl.error(\"You didn't enter a query...\");\r\n } else {\r\n //Reset values because they might've been set from the last search\r\n $scope.title = undefined;\r\n $scope.tmdbid = undefined;\r\n $scope.tvdbid = undefined;\r\n $scope.season = undefined;\r\n $scope.episode = undefined;\r\n $scope.goToSearchUrl();\r\n }\r\n };\r\n\r\n\r\n $scope.autocompleteActive = function () {\r\n return $scope.category.supportsById;\r\n };\r\n\r\n $scope.seriesSelected = function () {\r\n return $scope.category.name.indexOf(\"tv\") > -1;\r\n };\r\n\r\n $scope.toggleIndexer = function (indexer) {\r\n $scope.indexers[indexer] = !$scope.indexers[indexer]\r\n };\r\n\r\n\r\n function isIndexerPreselected(indexer) {\r\n if (angular.isUndefined($scope.indexers)) {\r\n return indexer.preselect;\r\n } else {\r\n return _.contains($scope.indexers, indexer.name);\r\n }\r\n\r\n }\r\n\r\n\r\n function getAvailableIndexers() {\r\n return _.chain(safeConfig.indexers).filter(function (indexer) {\r\n return indexer.enabled && indexer.showOnSearch && (angular.isUndefined(indexer.categories) || indexer.categories.length == 0 || $scope.category.name == \"all\" || indexer.categories.indexOf($scope.category.name) > -1);\r\n }).sortBy(function (indexer) {\r\n return indexer.name.toLowerCase();\r\n })\r\n .map(function (indexer) {\r\n return {name: indexer.name, activated: isIndexerPreselected(indexer), categories: indexer.categories};\r\n }).value();\r\n }\r\n\r\n\r\n $scope.toggleAllIndexers = function () {\r\n angular.forEach($scope.availableIndexers, function (indexer) {\r\n indexer.activated = !indexer.activated;\r\n })\r\n };\r\n\r\n $scope.searchInputChanged = function () {\r\n $scope.$broadcast(\"searchInputChanged\", $scope.query != $stateParams.query ? $scope.query : null, $scope.minage, $scope.maxage, $scope.minsize, $scope.maxsize);\r\n };\r\n\r\n\r\n $scope.formatRequest = function (request) {\r\n return $sce.trustAsHtml(SearchHistoryService.formatRequest(request, false, true, true, true));\r\n };\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n\r\n\r\n function getAndSetSearchRequests() {\r\n SearchHistoryService.getSearchHistoryForSearching().then(function (data) {\r\n $scope.searchHistory = data.data.searchRequests;\r\n });\r\n }\r\n\r\n if ($scope.mode) {\r\n $scope.startSearch();\r\n } else {\r\n //Getting the search history only makes sense when we're not currently searching\r\n getAndSetSearchRequests();\r\n }\r\n\r\n $scope.$on(\"searchResultsShown\", function() {\r\n getAndSetSearchRequests();\r\n });\r\n\r\n\r\n\r\n\r\n}\r\nSearchController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\", \"$window\", \"$filter\", \"$sce\", \"growl\", \"SearchService\", \"focus\", \"ConfigService\", \"CategoriesService\", \"blockUI\", \"$element\", \"ModalService\", \"SearchHistoryService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('RestartService', RestartService);\r\n\r\nfunction RestartService(blockUI, $timeout, $window, growl, NzbHydraControlService) {\r\n\r\n return {\r\n restart: restart,\r\n countdown: countdown\r\n };\r\n\r\n\r\n function internalCaR(message, timer) {\r\n\r\n if (timer >= 1) {\r\n blockUI.start(message + \"Restarting. Will reload page in \" + timer + \" seconds...\");\r\n $timeout(function () {\r\n internalCaR(message, timer - 1)\r\n }, 1000);\r\n } else {\r\n $timeout(function () {\r\n blockUI.start(\"Reloading page...\");\r\n $window.location.reload();\r\n }, 1000);\r\n }\r\n }\r\n \r\n function countdown() {\r\n internalCaR(\"\", 15);\r\n }\r\n\r\n function restart(message) {\r\n message = angular.isDefined(message) ? message + \" \" : \"\";\r\n NzbHydraControlService.restart().then(internalCaR(message, 15),\r\n function () {\r\n growl.info(\"Unable to send restart command.\");\r\n }\r\n )\r\n }\r\n}\r\nRestartService.$inject = [\"blockUI\", \"$timeout\", \"$window\", \"growl\", \"NzbHydraControlService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('NzbHydraControlService', NzbHydraControlService);\r\n\r\nfunction NzbHydraControlService($http) {\r\n\r\n return {\r\n restart: restart,\r\n shutdown: shutdown,\r\n deleteLogAndDb: deleteLogAndDb\r\n };\r\n\r\n function restart() {\r\n return $http.get(\"internalapi/restart\");\r\n }\r\n\r\n function shutdown() {\r\n return $http.get(\"internalapi/shutdown\");\r\n }\r\n\r\n function deleteLogAndDb() {\r\n return $http.get(\"internalapi/deleteloganddb\");\r\n }\r\n}\r\nNzbHydraControlService.$inject = [\"$http\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('NzbDownloadService', NzbDownloadService);\r\n\r\nfunction NzbDownloadService($http, ConfigService, DownloaderCategoriesService) {\r\n\r\n var service = {\r\n download: download,\r\n getEnabledDownloaders: getEnabledDownloaders\r\n };\r\n\r\n return service;\r\n\r\n function sendNzbAddCommand(downloader, searchresultids, category) {\r\n var params = {downloader: downloader.name, searchresultids: angular.toJson(searchresultids)};\r\n if (category != \"No category\") {\r\n params[\"category\"] = category;\r\n }\r\n return $http.put(\"internalapi/addnzbs\", params);\r\n }\r\n \r\n function download(downloader, searchresultids) {\r\n \r\n var category = downloader.defaultCategory;\r\n \r\n if ((_.isUndefined(category) || category == \"\" || category == null) && category != \"No category\") {\r\n return DownloaderCategoriesService.openCategorySelection(downloader).then(function (category) {\r\n return sendNzbAddCommand(downloader, searchresultids, category)\r\n }, function (error) {\r\n throw error;\r\n });\r\n } else {\r\n return sendNzbAddCommand(downloader, searchresultids, category)\r\n }\r\n }\r\n \r\n function getEnabledDownloaders() {\r\n return _.filter(ConfigService.getSafe().downloaders, \"enabled\");\r\n }\r\n}\r\nNzbDownloadService.$inject = [\"$http\", \"ConfigService\", \"DownloaderCategoriesService\"];\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('ModalService', ModalService);\r\n\r\nfunction ModalService($uibModal, $q) {\r\n \r\n return {\r\n open: open\r\n };\r\n \r\n function open(headline, message, params, size) {\r\n //params example:\r\n /*\r\n var p =\r\n {\r\n yes: {\r\n text: \"Yes\", //default: Ok\r\n onYes: function() {}\r\n },\r\n no: { //default: Empty\r\n text: \"No\",\r\n onNo: function () {\r\n }\r\n },\r\n cancel: { \r\n text: \"Cancel\", //default: Cancel\r\n onCancel: function () {\r\n }\r\n }\r\n };\r\n */\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'static/html/modal.html',\r\n controller: 'ModalInstanceCtrl',\r\n size: angular.isDefined(size) ? size : \"md\",\r\n resolve: {\r\n headline: function () {\r\n return headline;\r\n },\r\n message: function(){ \r\n return message;\r\n },\r\n params: function() {\r\n return params;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then(function() {\r\n \r\n }, function() {\r\n \r\n });\r\n }\r\n \r\n}\r\nModalService.$inject = [\"$uibModal\", \"$q\"];\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('ModalInstanceCtrl', ModalInstanceCtrl);\r\n\r\nfunction ModalInstanceCtrl($scope, $uibModalInstance, headline, message, params) {\r\n\r\n $scope.message = message;\r\n $scope.headline = headline;\r\n $scope.params = params;\r\n $scope.showCancel = angular.isDefined(params) && angular.isDefined(params.cancel);\r\n $scope.showNo = angular.isDefined(params) && angular.isDefined(params.no);\r\n\r\n if (angular.isUndefined(params) || angular.isUndefined(params.yes)) {\r\n $scope.params = {\r\n yes: {\r\n text: \"Ok\"\r\n }\r\n }\r\n } else if (angular.isUndefined(params.yes.text)) {\r\n params.yes.text = \"Yes\";\r\n }\r\n \r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isUndefined($scope.params.no.text)) {\r\n $scope.params.no.text = \"No\";\r\n }\r\n \r\n if (angular.isDefined(params) && angular.isDefined(params.cancel) && angular.isUndefined($scope.params.cancel.text)) {\r\n $scope.params.cancel.text = \"Cancel\";\r\n }\r\n\r\n $scope.yes = function () {\r\n $uibModalInstance.close();\r\n if(angular.isDefined(params) && angular.isDefined(params.yes) && angular.isDefined($scope.params.yes.onYes)) {\r\n $scope.params.yes.onYes();\r\n }\r\n };\r\n\r\n $scope.no = function () {\r\n $uibModalInstance.close();\r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isDefined($scope.params.no.onNo)) {\r\n $scope.params.no.onNo();\r\n }\r\n };\r\n\r\n $scope.cancel = function () {\r\n $uibModalInstance.dismiss();\r\n if (angular.isDefined(params.cancel) && angular.isDefined($scope.params.cancel.onCancel)) {\r\n $scope.params.cancel.onCancel();\r\n }\r\n };\r\n\r\n $scope.$on(\"modal.closing\", function (targetScope, reason, c) {\r\n if (reason == \"backdrop click\") {\r\n $scope.cancel();\r\n }\r\n });\r\n}\r\nModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"headline\", \"message\", \"params\"];\r\n","angular\n .module('nzbhydraApp')\n .service('GeneralModalService', GeneralModalService);\n\nfunction GeneralModalService() {\n \n \n this.open = function (msg, template, templateUrl, size, data) {\n \n //Prevent circular dependency\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\n var $uibModal = myInjector.get(\"$uibModal\");\n var params = {};\n \n if(angular.isUndefined(size)) {\n params[\"size\"] = size;\n }\n if (angular.isUndefined(template)) {\n if (angular.isUndefined(templateUrl)) {\n params[\"template\"] = '
' + msg + '
';\n } else {\n params[\"templateUrl\"] = templateUrl;\n }\n } else {\n params[\"template\"] = template;\n }\n params[\"resolve\"] = \n {\n data: function () {\n return data;\n }\n };\n \n var modalInstance = $uibModal.open(params);\n\n modalInstance.result.then();\n\n };\n \n \n}","angular\r\n .module('nzbhydraApp')\r\n .controller('LoginController', LoginController);\r\n\r\nfunction LoginController($scope, RequestsErrorHandler, $state, HydraAuthService, growl) {\r\n $scope.user = {};\r\n $scope.login = function () {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n HydraAuthService.login($scope.user.username, $scope.user.password).then(function () {\r\n HydraAuthService.setLoggedInByForm();\r\n growl.info(\"Login successful!\");\r\n $state.go(\"root.search\");\r\n }, function () {\r\n growl.error(\"Login failed!\")\r\n });\r\n });\r\n }\r\n}\r\nLoginController.$inject = [\"$scope\", \"RequestsErrorHandler\", \"$state\", \"HydraAuthService\", \"growl\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('IndexerStatusesController', IndexerStatusesController);\r\n\r\n function IndexerStatusesController($scope, $http, statuses) {\r\n $scope.statuses = statuses.data.indexerStatuses;\r\n \r\n $scope.isInPast = function (timestamp) {\r\n return timestamp * 1000 < (new Date).getTime();\r\n };\r\n \r\n $scope.enable = function(indexerName) {\r\n $http.get(\"internalapi/enableindexer\", {params: {name: indexerName}}).then(function(response){\r\n $scope.statuses = response.data.indexerStatuses;\r\n });\r\n }\r\n\r\n }\r\n IndexerStatusesController.$inject = [\"$scope\", \"$http\", \"statuses\"];\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('formatDate', formatDate);\r\n\r\nfunction formatDate(dateFilter) {\r\n return function(timestamp, hidePast) {\r\n if (timestamp) {\r\n if (timestamp * 1000 < (new Date).getTime() && hidePast) {\r\n return \"\"; //\r\n }\r\n \r\n var t = timestamp * 1000;\r\n t = dateFilter(t, 'yyyy-MM-dd HH:mm');\r\n return t;\r\n } else {\r\n return \"\";\r\n }\r\n }\r\n}\r\nformatDate.$inject = [\"dateFilter\"];\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('reformatDate', reformatDate);\r\n\r\nfunction reformatDate() {\r\n return function (date) {\r\n //Date in database is saved as UTC without timezone information\r\n return moment.utc(date, \"ddd, D MMM YYYY HH:mm:ss z\").local().format(\"YYYY-MM-DD HH:mm\");\r\n \r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .controller('IndexController', IndexController);\r\n\r\nfunction IndexController($scope, $http, $stateParams, $state) {\r\n console.log(\"Index\");\r\n $state.go(\"root.search\");\r\n}\r\nIndexController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('HydraAuthService', HydraAuthService);\r\n\r\nfunction HydraAuthService($q, $rootScope, $http, $cookies, bootstrapped) {\r\n\r\n var loggedIn = bootstrapped.username;\r\n\r\n \r\n return {\r\n isLoggedIn: isLoggedIn,\r\n login: login,\r\n askForPassword: askForPassword,\r\n logout: logout,\r\n setLoggedInByForm: setLoggedInByForm,\r\n getUserRights: getUserRights,\r\n setLoggedInByBasic: setLoggedInByBasic,\r\n getUserName: getUserName,\r\n getUserInfos: getUserInfos\r\n };\r\n\r\n\r\n\r\n function getUserInfos() {\r\n return bootstrapped;\r\n }\r\n\r\n \r\n function isLoggedIn() {\r\n return bootstrapped.username;\r\n }\r\n \r\n function setLoggedInByForm() {\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n }\r\n\r\n function setLoggedInByBasic(_maySeeStats, _maySeeAdmin, _username) {\r\n }\r\n \r\n function login(username, password) {\r\n var deferred = $q.defer();\r\n return $http.post(\"auth/login\", data = {username: username, password: password}).then(function (data) {\r\n bootstrapped = data.data;\r\n loggedIn = true;\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n deferred.resolve();\r\n });\r\n return deferred;\r\n }\r\n\r\n function askForPassword(params) {\r\n return $http.get(\"internalapi/askforpassword\", {params: params}).then(function (data) {\r\n bootstrapped = data.data;\r\n return bootstrapped;\r\n });\r\n\r\n }\r\n \r\n function logout() {\r\n var deferred = $q.defer();\r\n return $http.post(\"auth/logout\").then(function(data) {\r\n $rootScope.$broadcast(\"user:loggedOut\");\r\n bootstrapped = data.data;\r\n loggedIn = false;\r\n deferred.resolve();\r\n });\r\n return deferred;\r\n }\r\n \r\n function getUserRights() {\r\n var userInfos = getUserInfos();\r\n return {maySeeStats: userInfos.maySeeStats, maySeeAdmin: userInfos.maySeeAdmin, maySeeSearch: userInfos.maySeeSearch};\r\n }\r\n \r\n function getUserName() {\r\n return bootstrapped.username;\r\n }\r\n\r\n\r\n \r\n \r\n \r\n \r\n}\r\nHydraAuthService.$inject = [\"$q\", \"$rootScope\", \"$http\", \"$cookies\", \"bootstrapped\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('HeaderController', HeaderController);\r\n\r\nfunction HeaderController($scope, $state, $http, growl, HydraAuthService) {\r\n\r\n\r\n $scope.showLoginout = false;\r\n $scope.oldUserName = null;\r\n\r\n function update() {\r\n\r\n $scope.userInfos = HydraAuthService.getUserInfos();\r\n if (!$scope.userInfos.authConfigured) {\r\n $scope.showAdmin = true;\r\n $scope.showStats = true;\r\n $scope.showLoginout = false;\r\n } else {\r\n if ($scope.userInfos.username) {\r\n $scope.showAdmin = $scope.userInfos.maySeeAdmin || !$scope.userInfos.adminRestricted;\r\n $scope.showStats = $scope.userInfos.maySeeStats || !$scope.userInfos.statsRestricted;\r\n $scope.showLoginout = true;\r\n $scope.username = $scope.userInfos.username;\r\n $scope.loginlogoutText = \"Logout \" + $scope.username;\r\n $scope.oldUserName = $scope.username;\r\n } else {\r\n $scope.showAdmin = !$scope.userInfos.adminRestricted;\r\n $scope.showStats = !$scope.userInfos.statsRestricted;\r\n $scope.loginlogoutText = \"Login\";\r\n $scope.showLoginout = $scope.userInfos.adminRestricted || $scope.userInfos.statsRestricted || $scope.userInfos.searchRestricted;\r\n $scope.username = \"\";\r\n }\r\n }\r\n }\r\n\r\n update();\r\n\r\n\r\n $scope.$on(\"user:loggedIn\", function (event, data) {\r\n update();\r\n });\r\n\r\n $scope.$on(\"user:loggedOut\", function (event, data) {\r\n update();\r\n });\r\n\r\n $scope.loginout = function () {\r\n if (HydraAuthService.isLoggedIn()) {\r\n HydraAuthService.logout().then(function () {\r\n if ($scope.userInfos.authType == \"basic\") {\r\n growl.info(\"Logged out. Close your browser to make sure session is closed.\");\r\n }\r\n else if ($scope.userInfos.authType == \"form\") {\r\n growl.info(\"Logged out\");\r\n }\r\n update();\r\n //$state.go(\"root.search\", null, {reload: true});\r\n });\r\n\r\n } else {\r\n if ($scope.userInfos.authType == \"basic\") {\r\n var params = {};\r\n if ($scope.oldUserName) {\r\n params = {\r\n old_username: $scope.oldUserName\r\n }\r\n }\r\n HydraAuthService.askForPassword(params).then(function () {\r\n growl.info(\"Login successful!\");\r\n update();\r\n $scope.oldUserName = null;\r\n $state.go(\"root.search\");\r\n })\r\n } else if ($scope.userInfos.authType == \"form\") {\r\n $state.go(\"root.login\");\r\n } else {\r\n growl.info(\"You shouldn't need to login but here you go!\");\r\n }\r\n }\r\n }\r\n}\r\nHeaderController.$inject = [\"$scope\", \"$state\", \"$http\", \"growl\", \"HydraAuthService\"];\r\n","var HEADER_NAME = 'MyApp-Handle-Errors-Generically';\nvar specificallyHandleInProgress = false;\n\nnzbhydraapp.factory('RequestsErrorHandler', [\"$q\", \"growl\", \"blockUI\", \"GeneralModalService\", function ($q, growl, blockUI, GeneralModalService) {\n return {\n // --- The user's API for claiming responsiblity for requests ---\n specificallyHandled: function (specificallyHandledBlock) {\n specificallyHandleInProgress = true;\n try {\n return specificallyHandledBlock();\n } finally {\n specificallyHandleInProgress = false;\n }\n },\n\n // --- Response interceptor for handling errors generically ---\n responseError: function (rejection) {\n blockUI.reset();\n var shouldHandle = (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && !rejection.config.url.contains(\"logerror\"));\n if (shouldHandle) {\n var message = \"An error occured :
\" + rejection.status + \": \" + rejection.statusText;\n\n if (rejection.data) {\n message += \"

\" + rejection.data;\n }\n GeneralModalService.open(message);\n\n } else if (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && rejection.config.url.contains(\"logerror\")) {\n console.log(\"Not handling connection error while sending exception to server\");\n }\n\n return $q.reject(rejection);\n }\n };\n}]);\n\n\nnzbhydraapp.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {\n $httpProvider.interceptors.push('RequestsErrorHandler');\n\n // --- Decorate $http to add a special header by default ---\n\n function addHeaderToConfig(config) {\n config = config || {};\n config.headers = config.headers || {};\n\n // Add the header unless user asked to handle errors himself\n if (!specificallyHandleInProgress) {\n config.headers[HEADER_NAME] = true;\n }\n\n return config;\n }\n\n // The rest here is mostly boilerplate needed to decorate $http safely\n $provide.decorator('$http', ['$delegate', function ($delegate) {\n function decorateRegularCall(method) {\n return function (url, config) {\n return $delegate[method](url, addHeaderToConfig(config));\n };\n }\n\n function decorateDataCall(method) {\n return function (url, data, config) {\n return $delegate[method](url, data, addHeaderToConfig(config));\n };\n }\n\n function copyNotOverriddenAttributes(newHttp) {\n for (var attr in $delegate) {\n if (!newHttp.hasOwnProperty(attr)) {\n if (typeof($delegate[attr]) === 'function') {\n newHttp[attr] = function () {\n return $delegate.apply($delegate, arguments);\n };\n } else {\n newHttp[attr] = $delegate[attr];\n }\n }\n }\n }\n\n var newHttp = function (config) {\n return $delegate(addHeaderToConfig(config));\n };\n\n newHttp.get = decorateRegularCall('get');\n newHttp.delete = decorateRegularCall('delete');\n newHttp.head = decorateRegularCall('head');\n newHttp.jsonp = decorateRegularCall('jsonp');\n newHttp.post = decorateDataCall('post');\n newHttp.put = decorateDataCall('put');\n\n copyNotOverriddenAttributes(newHttp);\n\n return newHttp;\n }]);\n}]);","hashCode = function (s) {\r\n return s.split(\"\").reduce(function (a, b) {\r\n a = ((a << 5) - a) + b.charCodeAt(0);\r\n return a & a\r\n }, 0);\r\n};\r\n\r\nangular\r\n .module('nzbhydraApp').run([\"formlyConfig\", \"formlyValidationMessages\", function (formlyConfig, formlyValidationMessages) {\r\n formlyValidationMessages.addStringMessage('required', 'This field is required');\r\n formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'fc.$touched || form.$submitted';\r\n\r\n}]);\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\r\n formlyConfigProvider.extras.removeChromeAutoComplete = true;\r\n formlyConfigProvider.extras.explicitAsync = true;\r\n formlyConfigProvider.disableWarnings = window.onProd;\r\n\r\n\r\n formlyConfigProvider.setWrapper({\r\n name: 'settingWrapper',\r\n templateUrl: 'setting-wrapper.html'\r\n });\r\n\r\n\r\n formlyConfigProvider.setWrapper({\r\n name: 'fieldset',\r\n template: [\r\n '
',\r\n '{{options.templateOptions.label}}',\r\n '',\r\n '
'\r\n ].join(' ')\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'help',\r\n template: [\r\n '
',\r\n '
',\r\n '
{{ line }}
',\r\n '
',\r\n '
'\r\n ].join(' ')\r\n });\r\n\r\n\r\n formlyConfigProvider.setWrapper({\r\n name: 'logicalGroup',\r\n template: [\r\n ''\r\n ].join(' ')\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalInput',\r\n extends: 'input',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'timeOfDay',\r\n extends: 'horizontalInput',\r\n controller: ['$scope', function ($scope) {\r\n $scope.model[$scope.options.key] = moment.utc($scope.model[$scope.options.key]).toDate();\r\n }]\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'percentInput',\r\n template: [\r\n ''\r\n ].join(' ')\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'apiKeyInput',\r\n template: [\r\n '
',\r\n '',\r\n '',\r\n '',\r\n '
'\r\n ].join(' '),\r\n controller: function ($scope) {\r\n $scope.generate = function () {\r\n $scope.model[$scope.options.key] = (Math.random() * 1e32).toString(36);\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'testConnection',\r\n templateUrl: 'button-test-connection.html'\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalTestConnection',\r\n extends: 'testConnection',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'checkCaps',\r\n templateUrl: 'button-check-caps.html',\r\n controller: function ($scope, ConfigBoxService) {\r\n $scope.message = \"\";\r\n $scope.uniqueId = hashCode($scope.model.name) + hashCode($scope.model.host);\r\n\r\n var testButton = \"#button-check-caps-\" + $scope.uniqueId;\r\n var testMessage = \"#message-check-caps-\" + $scope.uniqueId;\r\n\r\n function showSuccess() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-danger\");\r\n angular.element(testButton).addClass(\"btn-success\");\r\n }\r\n\r\n function showError() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-success\");\r\n angular.element(testButton).addClass(\"btn-danger\");\r\n }\r\n\r\n $scope.checkCaps = function () {\r\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\r\n\r\n var url = \"internalapi/test_caps\";\r\n var params = {indexer: $scope.model.name, apikey: $scope.model.apikey, host: $scope.model.host};\r\n if (angular.isDefined($scope.model.username)) {\r\n params[\"username\"] = $scope.model.username;\r\n params[\"password\"] = $scope.model.password;\r\n }\r\n ConfigBoxService.checkCaps(url, params, $scope.model).then(function (data, model) {\r\n angular.element(testMessage).text(\"Supports: \" + data.supportedIds + \",\" ? data.supportedIds && data.supportedTypes : \"\" + data.supportedTypes);\r\n showSuccess();\r\n }, function (message) {\r\n angular.element(testMessage).text(message);\r\n showError();\r\n }).finally(function () {\r\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\r\n });\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalCheckCaps',\r\n extends: 'checkCaps',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalApiKeyInput',\r\n extends: 'apiKeyInput',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalPercentInput',\r\n extends: 'percentInput',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'switch',\r\n template: \r\n '
'\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'duoSetting',\r\n extends: 'input',\r\n defaultOptions: {\r\n className: 'col-md-9',\r\n templateOptions: {\r\n type: 'number',\r\n noRow: true,\r\n label: ''\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalSwitch',\r\n extends: 'switch',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalSelect',\r\n extends: 'select',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalMultiselect',\r\n defaultOptions: {\r\n templateOptions: {\r\n optionsAttr: 'bs-options',\r\n ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',\r\n valueProp: 'id',\r\n labelProp: 'label',\r\n getPlaceholder: function() {return \"\";}\r\n }\r\n },\r\n templateUrl: 'ui-select-multiple.html',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'label',\r\n template: ''\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'duolabel',\r\n extends: 'label',\r\n defaultOptions: {\r\n className: 'col-md-2',\r\n templateOptions: {\r\n label: '-'\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'repeatSection',\r\n templateUrl: 'repeatSection.html',\r\n controller: function ($scope) {\r\n $scope.formOptions = {formState: $scope.formState};\r\n $scope.addNew = addNew;\r\n $scope.remove = remove;\r\n $scope.copyFields = copyFields;\r\n\r\n function copyFields(fields) {\r\n fields = angular.copy(fields);\r\n $scope.repeatfields = fields;\r\n return fields;\r\n }\r\n\r\n $scope.clear = function (field) {\r\n return _.mapObject(field, function (key, val) {\r\n if (typeof val === 'object') {\r\n return $scope.clear(val);\r\n }\r\n return undefined;\r\n\r\n });\r\n };\r\n\r\n\r\n function addNew() {\r\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\r\n var repeatsection = $scope.model[$scope.options.key];\r\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\r\n repeatsection.push(newsection);\r\n }\r\n\r\n function remove($index) {\r\n $scope.model[$scope.options.key].splice($index, 1);\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'arrayConfig',\r\n templateUrl: 'arrayConfig.html',\r\n controller: function ($scope, $uibModal, growl) {\r\n $scope.formOptions = {formState: $scope.formState};\r\n $scope._showBox = _showBox;\r\n $scope.showBox = showBox;\r\n $scope.isInitial = false;\r\n\r\n $scope.presets = $scope.options.data.presets($scope.model);\r\n\r\n\r\n function _showBox(model, parentModel, isInitial, callback) {\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'configBox.html',\r\n controller: 'ConfigBoxInstanceController',\r\n size: 'lg',\r\n resolve: {\r\n model: function () {\r\n return model;\r\n },\r\n fields: function () {\r\n return $scope.options.data.fieldsFunction(model, parentModel, isInitial, angular.injector());\r\n },\r\n isInitial: function () {\r\n return isInitial\r\n },\r\n parentModel: function () {\r\n return parentModel;\r\n },\r\n data: function () {\r\n return $scope.options.data;\r\n }\r\n }\r\n });\r\n\r\n\r\n modalInstance.result.then(function () {\r\n $scope.form.$setDirty(true);\r\n if (angular.isDefined(callback)) {\r\n callback(true);\r\n }\r\n }, function () {\r\n if (angular.isDefined(callback)) {\r\n callback(false);\r\n }\r\n });\r\n }\r\n\r\n function showBox(model, parentModel) {\r\n $scope._showBox(model, parentModel, false)\r\n }\r\n\r\n $scope.addEntry = function (entriesCollection, preset) {\r\n if ($scope.options.data.checkAddingAllowed(entriesCollection, preset)) {\r\n var model = angular.copy($scope.options.data.defaultModel);\r\n if (angular.isDefined(preset)) {\r\n _.extend(model, preset);\r\n }\r\n\r\n $scope.isInitial = true;\r\n\r\n $scope._showBox(model, entriesCollection, true, function (isSubmitted) {\r\n if (isSubmitted) {\r\n entriesCollection.push(model);\r\n }\r\n });\r\n } else {\r\n growl.error(\"That predefined indexer is already configured.\"); //For now this is the only case where adding is forbidden so we use this hardcoded message \"for now\"... (;-))\r\n }\r\n\r\n };\r\n\r\n }\r\n\r\n });\r\n\r\n }]);\r\n\r\n\r\nangular.module('nzbhydraApp').controller('ConfigBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$http\", \"model\", \"fields\", \"isInitial\", \"parentModel\", \"data\", \"growl\", function ($scope, $q, $uibModalInstance, $http, model, fields, isInitial, parentModel, data, growl) {\r\n\r\n $scope.model = model;\r\n $scope.fields = fields;\r\n $scope.isInitial = isInitial;\r\n $scope.allowDelete = data.allowDeleteFunction(model);\r\n $scope.spinnerActive = false;\r\n $scope.needsConnectionTest = false;\r\n \r\n $scope.obSubmit = function () {\r\n console.log($scope);\r\n if ($scope.form.$valid) {\r\n \r\n var a = data.checkBeforeClose($scope, model).then(function() {\r\n $uibModalInstance.close($scope);\r\n });\r\n } else {\r\n growl.error(\"Config invalid. Please check your settings.\");\r\n angular.forEach($scope.form.$error, function (error) {\r\n angular.forEach(error, function (field) {\r\n field.$setTouched();\r\n });\r\n });\r\n }\r\n };\r\n\r\n $scope.reset = function () {\r\n $scope.reset();\r\n };\r\n\r\n $scope.deleteEntry = function () {\r\n parentModel.splice(parentModel.indexOf(model), 1);\r\n $uibModalInstance.close($scope);\r\n };\r\n\r\n $scope.reset = function () {\r\n if (angular.isDefined(data.resetFunction)) {\r\n data.resetFunction($scope);\r\n }\r\n };\r\n\r\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\r\n if (reason == \"backdrop click\") {\r\n $scope.reset($scope);\r\n }\r\n });\r\n}]);\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('ConfigBoxService', ConfigBoxService);\r\n\r\nfunction ConfigBoxService($http, $q) {\r\n\r\n return {\r\n checkConnection: checkConnection,\r\n checkCaps: checkCaps\r\n };\r\n\r\n function checkConnection(url, settings) {\r\n var deferred = $q.defer();\r\n\r\n $http.post(url, settings).success(function (result) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \r\n if (result.result) {\r\n deferred.resolve();\r\n } else {\r\n deferred.reject({checked: true, message: result.message});\r\n }\r\n }).error(function (result) {\r\n deferred.reject({checked: false, message: result.message});\r\n });\r\n\r\n return deferred.promise;\r\n }\r\n\r\n function checkCaps(url, params, model) {\r\n var deferred = $q.defer();\r\n\r\n $http.post(url, params).success(function (data) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \r\n if (data.success) {\r\n model.search_ids = data.supportedIds;\r\n model.searchTypes = data.supportedTypes;\r\n if (data.supportsAllCategories) { //Don't display all the categories, will be replaced with placeholder \"All categories\"\r\n model.categories = [];\r\n } else {\r\n model.categories = data.supportedCategories;\r\n }\r\n model.animeCategory = data.animeCategory;\r\n model.audiobookCategory = data.audiobookCategory;\r\n model.comicCategory = data.comicCategory;\r\n model.ebookCategory = data.ebookCategory;\r\n model.magazineCategory = data.magazineCategory;\r\n model.backend = data.backend;\r\n deferred.resolve({supportedIds: data.supportedIds, supportedTypes: data.supportedTypes}, model);\r\n } else {\r\n deferred.reject(data.message);\r\n }\r\n }).error(function () {\r\n deferred.reject(\"Unknown error\");\r\n });\r\n\r\n return deferred.promise;\r\n }\r\n\r\n}\r\nConfigBoxService.$inject = [\"$http\", \"$q\"];\r\n\r\n\r\n\r\n\r\n","var filters = angular.module('filters', []);\r\n\r\nfilters.filter('bytes', function() {\r\n\treturn function(bytes) {\r\n\t\treturn filesize(bytes);\r\n\t}\r\n});\r\n\r\nfilters.filter('unsafe', \r\n\t[\"$sce\", function ($sce) {\r\n\t\treturn function (value, type) {\r\n\t\t\treturn $sce.trustAs(type || 'html', text);\r\n\t\t};\r\n\t}]\r\n);\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('DownloaderCategoriesService', DownloaderCategoriesService);\r\n\r\nfunction DownloaderCategoriesService($http, $q, $uibModal) {\r\n\r\n var categories = {};\r\n var selectedCategory = {};\r\n\r\n var service = {\r\n get: getCategories,\r\n invalidate: invalidate,\r\n select: select,\r\n openCategorySelection: openCategorySelection\r\n };\r\n\r\n var deferred;\r\n\r\n return service;\r\n\r\n\r\n function getCategories(downloader) {\r\n\r\n function loadAll() {\r\n if (angular.isDefined(categories) && angular.isDefined(categories.downloader)) {\r\n var deferred = $q.defer();\r\n deferred.resolve(categories.downloader);\r\n return deferred.promise;\r\n }\r\n \r\n return $http.get('internalapi/getcategories', {params: {downloader: downloader.name}})\r\n .then(function (categoriesResponse) {\r\n \r\n console.log(\"Updating downloader categories cache\");\r\n var categories = {downloader: categoriesResponse.data.categories};\r\n return categoriesResponse.data.categories;\r\n\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n return loadAll().then(function (categories) {\r\n return categories;\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n\r\n function openCategorySelection(downloader) {\r\n $uibModal.open({\r\n templateUrl: 'static/html/directives/addable-nzb-modal.html',\r\n controller: 'DownloaderCategorySelectionController',\r\n size: \"sm\",\r\n resolve: {\r\n categories: function () {\r\n return getCategories(downloader)\r\n }\r\n }\r\n });\r\n deferred = $q.defer();\r\n return deferred.promise;\r\n }\r\n\r\n function select(category) {\r\n selectedCategory = category;\r\n console.log(\"Selected category \" + category);\r\n deferred.resolve(category);\r\n }\r\n\r\n function invalidate() {\r\n console.log(\"Invalidating categories\");\r\n categories = undefined;\r\n }\r\n}\r\nDownloaderCategoriesService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\r\n\r\nangular\r\n .module('nzbhydraApp').controller('DownloaderCategorySelectionController', [\"$scope\", \"$uibModalInstance\", \"DownloaderCategoriesService\", \"categories\", function ($scope, $uibModalInstance, DownloaderCategoriesService, categories) {\r\n console.log(categories);\r\n $scope.categories = categories;\r\n $scope.select = function (category) {\r\n DownloaderCategoriesService.select(category);\r\n $uibModalInstance.close($scope);\r\n }\r\n}]);","angular\n .module('nzbhydraApp')\n .controller('DownloadHistoryController', DownloadHistoryController);\n\n\nfunction DownloadHistoryController($scope, StatsService, downloads, ConfigService) {\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n $scope.sortModel = {\n column: \"time\",\n sortMode: 2\n };\n $scope.filterModel = {};\n\n //Filter options\n $scope.indexersForFiltering = [];\n _.forEach(ConfigService.getSafe().indexers, function (indexer) {\n $scope.indexersForFiltering.push({label: indexer.name, id: indexer.name})\n });\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\n $scope.successfulForFiltering = [{label: \"Succesful\", id: true}, {label: \"Unsuccesful\", id: false}, {label: \"Unknown\", id: null}];\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: false}, {label: \"Internal\", value: true}];\n\n\n //Preloaded data\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n\n\n $scope.update = function () {\n StatsService.getDownloadHistory($scope.pagination.current, $scope.limit, $scope.filterModel, $scope.sortModel).then(function (downloads) {\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n });\n };\n\n\n $scope.$on(\"sort\", function (event, column, sortMode) {\n if (sortMode == 0) {\n column = \"time\";\n sortMode = 2;\n }\n $scope.sortModel = {\n column: column,\n sortMode: sortMode\n };\n $scope.$broadcast(\"newSortColumn\", column);\n $scope.update();\n });\n\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filter) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n $scope.update();\n })\n\n}\nDownloadHistoryController.$inject = [\"$scope\", \"StatsService\", \"downloads\", \"ConfigService\"];\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateEpoch', reformatDateEpoch);\n\nfunction reformatDateEpoch() {\n return function (date) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm\");\n\n }\n}","angular\r\n .module('nzbhydraApp')\r\n .factory('ConfigService', ConfigService);\r\n\r\nfunction ConfigService($http, $q, $cacheFactory, bootstrapped) {\r\n\r\n var cache = $cacheFactory(\"nzbhydra\");\r\n var safeConfig = bootstrapped.safeConfig;\r\n\r\n return {\r\n set: set,\r\n get: get,\r\n getSafe: getSafe,\r\n invalidateSafe: invalidateSafe,\r\n maySeeAdminArea: maySeeAdminArea\r\n };\r\n\r\n\r\n function set(newConfig) {\r\n $http.put('internalapi/setsettings', newConfig)\r\n .then(function (successresponse) {\r\n console.log(\"Settings saved. Updating cache\");\r\n cache.put(\"config\", newConfig);\r\n invalidateSafe();\r\n }, function (errorresponse) {\r\n console.log(\"Error saving settings: \" + errorresponse);\r\n });\r\n }\r\n\r\n function get() {\r\n var config = cache.get(\"config\");\r\n if (angular.isUndefined(config)) {\r\n config = $http.get('internalapi/getconfig').then(function (data) {\r\n return data.data;\r\n });\r\n cache.put(\"config\", config);\r\n }\r\n\r\n return config;\r\n }\r\n\r\n function getSafe() {\r\n return safeConfig;\r\n }\r\n\r\n function invalidateSafe() {\r\n $http.get('internalapi/getsafeconfig').then(function (data) {\r\n safeConfig = data.data;\r\n });\r\n }\r\n\r\n function maySeeAdminArea() {\r\n function loadAll() {\r\n var maySeeAdminArea = cache.get(\"maySeeAdminArea\");\r\n if (!angular.isUndefined(maySeeAdminArea)) {\r\n var deferred = $q.defer();\r\n deferred.resolve(maySeeAdminArea);\r\n return deferred.promise;\r\n }\r\n\r\n return $http.get('internalapi/mayseeadminarea')\r\n .then(function (configResponse) {\r\n var config = configResponse.data;\r\n cache.put(\"maySeeAdminArea\", config);\r\n return configResponse.data;\r\n });\r\n }\r\n\r\n return loadAll().then(function (maySeeAdminArea) {\r\n return maySeeAdminArea;\r\n });\r\n }\r\n}\r\nConfigService.$inject = [\"$http\", \"$q\", \"$cacheFactory\", \"bootstrapped\"];","angular\r\n .module('nzbhydraApp')\r\n .factory('ConfigFields', ConfigFields);\r\n\r\nfunction ConfigFields($injector) {\r\n\r\n var restartWatcher;\r\n\r\n return {\r\n getFields: getFields,\r\n setRestartWatcher: setRestartWatcher\r\n };\r\n\r\n function setRestartWatcher(restartWatcherFunction) {\r\n restartWatcher = restartWatcherFunction;\r\n }\r\n\r\n\r\n function restartListener(field, newValue, oldValue) {\r\n if (newValue != oldValue) {\r\n restartWatcher();\r\n }\r\n }\r\n\r\n\r\n function ipValidator() {\r\n return {\r\n expression: function ($viewValue, $modelValue) {\r\n var value = $modelValue || $viewValue;\r\n if (value) {\r\n return /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(value)\r\n || /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(value);\r\n }\r\n return true;\r\n },\r\n message: '$viewValue + \" is not a valid IP Address\"'\r\n };\r\n }\r\n\r\n function regexValidator(regex, message, prefixViewValue) {\r\n return {\r\n expression: function ($viewValue, $modelValue) {\r\n var value = $modelValue || $viewValue;\r\n if (value) {\r\n return regex.test(value);\r\n }\r\n return true;\r\n },\r\n message: (prefixViewValue ? '$viewValue + \" ' : '\" ') + message + '\"'\r\n };\r\n }\r\n\r\n\r\n function getCategoryFields() {\r\n var fields = [];\r\n var ConfigService = $injector.get(\"ConfigService\");\r\n var categories = ConfigService.getSafe().categories;\r\n fields.push({\r\n key: 'enableCategorySizes',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Category sizes',\r\n help: \"Preset min and max sizes depending on the selected category\"\r\n }\r\n });\r\n _.each(categories, function (category) {\r\n if (category.name != \"all\" && category.name != \"na\") {\r\n var categoryFields = [\r\n {\r\n key: \"categories.\" + category.name + '.requiredWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required words',\r\n placeholder: 'separate, with, commas, like, this'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.requiredRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required regex',\r\n help: 'Must be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.forbiddenWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden words',\r\n placeholder: 'separate, with, commas, like, this'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.forbiddenRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden regex',\r\n help: 'Must not be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.applyRestrictions',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Apply restrictions',\r\n options: [\r\n {name: 'Internal searches', value: 'internal'},\r\n {name: 'API searches', value: 'external'},\r\n {name: 'All searches', value: 'both'}\r\n ],\r\n help: \"For which type of search word restrictions will be applied\"\r\n }\r\n }\r\n ];\r\n categoryFields.push({\r\n wrapper: 'settingWrapper',\r\n templateOptions: {\r\n label: 'Size preset'\r\n },\r\n fieldGroup: [\r\n {\r\n key: \"categories.\" + category.name + '.min',\r\n type: 'duoSetting',\r\n templateOptions: {\r\n addonRight: {\r\n text: 'MB'\r\n }\r\n }\r\n },\r\n {\r\n type: 'duolabel'\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.max',\r\n type: 'duoSetting', templateOptions: {addonRight: {text: 'MB'}}\r\n }\r\n ]\r\n });\r\n categoryFields.push({\r\n key: \"categories.\" + category.name + '.newznabCategories',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Newznab categories',\r\n help: 'Map newznab categories to hydra categories',\r\n required: true\r\n },\r\n parsers: [function (value) {\r\n if (!value) {\r\n return value;\r\n }\r\n var arr = [];\r\n arr.push.apply(arr, value.split(\",\").map(Number));\r\n return arr;\r\n\r\n }]\r\n });\r\n categoryFields.push({\r\n key: \"categories.\" + category.name + '.ignoreResults',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Ignore results',\r\n options: [\r\n {name: 'For internal searches', value: 'internal'},\r\n {name: 'For API searches', value: 'external'},\r\n {name: 'Always', value: 'always'},\r\n {name: 'Never', value: 'never'}\r\n ],\r\n help: \"Ignore results from this category\"\r\n }\r\n });\r\n\r\n fields.push({\r\n wrapper: 'fieldset',\r\n templateOptions: {\r\n label: category.pretty\r\n },\r\n fieldGroup: categoryFields\r\n\r\n })\r\n }\r\n }\r\n );\r\n return fields;\r\n }\r\n\r\n function getFields(rootModel) {\r\n return {\r\n main: [\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Hosting'},\r\n fieldGroup: [\r\n {\r\n key: 'host',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Host',\r\n required: true,\r\n placeholder: 'IPv4 address to bind to',\r\n help: 'I strongly recommend using a reverse proxy instead of exposing this directly. Requires restart.'\r\n },\r\n validators: {\r\n ipAddress: ipValidator()\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'port',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Port',\r\n required: true,\r\n placeholder: '5050',\r\n help: 'Requires restart'\r\n },\r\n validators: {\r\n port: regexValidator(/^\\d{1,5}$/, \"is no valid port\", true)\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'urlBase',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'URL base',\r\n placeholder: '/nzbhydra',\r\n help: 'Set when using an external proxy. Call using a trailing slash, e.g. http://www.domain.com/nzbhydra/'\r\n },\r\n validators: {\r\n urlBase: regexValidator(/^\\/[\\w\\/]*$/, \"Base URL needs to start with a slash and must not end with one\")\r\n }\r\n },\r\n {\r\n key: 'externalUrl',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'External URL',\r\n placeholder: 'https://www.somedomain.com/nzbhydra/',\r\n help: 'Set to the full external URL so machines outside can use the generated NZB links.'\r\n }\r\n },\r\n {\r\n key: 'useLocalUrlForApiAccess',\r\n type: 'horizontalSwitch',\r\n hideExpression: '!model.externalUrl',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Use local address in API results',\r\n help: 'Disable to make API results use the external URL in NZB links.'\r\n }\r\n },\r\n {\r\n key: 'ssl',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Use SSL',\r\n help: 'I recommend using a reverse proxy instead of this. Requires restart.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'socksProxy',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'SOCKS proxy',\r\n placeholder: 'socks5://user:pass@127.0.0.1:1080',\r\n help: \"IPv4 only\"\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'httpProxy',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'HTTP proxy',\r\n placeholder: 'http://user:pass@10.0.0.1:1080',\r\n help: \"IPv4 only\"\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'httpsProxy',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'HTTPS proxy',\r\n placeholder: 'https://user:pass@10.0.0.1:1090',\r\n help: \"IPv4 only\"\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'sslcert',\r\n hideExpression: '!model.ssl',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'SSL certificate file',\r\n required: true,\r\n help: 'Requires restart.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'sslkey',\r\n hideExpression: '!model.ssl',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'SSL key file',\r\n required: true,\r\n help: 'Requires restart.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n }\r\n\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'UI'},\r\n fieldGroup: [\r\n\r\n {\r\n key: 'theme',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'Theme',\r\n help: 'Reload page after saving',\r\n options: [\r\n {name: 'Grey', value: 'grey'},\r\n {name: 'Bright', value: 'bright'},\r\n {name: 'Dark', value: 'dark'}\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Security'},\r\n fieldGroup: [\r\n\r\n {\r\n key: 'apikey',\r\n type: 'horizontalApiKeyInput',\r\n templateOptions: {\r\n label: 'API key',\r\n help: 'Remove to disable. Alphanumeric only'\r\n },\r\n validators: {\r\n apikey: regexValidator(/^[a-zA-Z0-9]*$/, \"API key must only contain numbers and digits\", false)\r\n }\r\n },\r\n {\r\n key: 'dereferer',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Dereferer',\r\n help: 'Redirect external links to hide your instance. Insert $s for target URL. Delete to disable.'\r\n }\r\n }\r\n ]\r\n },\r\n\r\n {\r\n wrapper: 'fieldset',\r\n key: 'logging',\r\n templateOptions: {label: 'Logging'},\r\n fieldGroup: [\r\n {\r\n key: 'logfilelevel',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'Logfile level',\r\n options: [\r\n {name: 'Critical', value: 'CRITICAL'},\r\n {name: 'Error', value: 'ERROR'},\r\n {name: 'Warning', value: 'WARNING'},\r\n {name: 'Info', value: 'INFO'},\r\n {name: 'Debug', value: 'DEBUG'}\r\n ]\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logfilename',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Log file',\r\n required: true\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'rolloverAtStart',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n label: 'Startup rollover',\r\n help: 'Starts a new log file on start/restart'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logMaxSize',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Max log file size',\r\n help: 'When log file size is reached a new one is started. Set to 0 to disable.',\r\n addonRight: {\r\n text: 'kB'\r\n }\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logRotateAfterDays',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Rotate after',\r\n help: 'A new log file is started after this many days. Supercedes max size. Keep empty to disable.',\r\n addonRight: {\r\n text: 'days'\r\n }\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'keepLogFiles',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Keep log files',\r\n help: 'Number of log files to keep before oldest is deleted.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n\r\n {\r\n key: 'consolelevel',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'Console log level',\r\n options: [\r\n {name: 'Critical', value: 'CRITICAL'},\r\n {name: 'Error', value: 'ERROR'},\r\n {name: 'Warning', value: 'WARNING'},\r\n {name: 'Info', value: 'INFO'},\r\n {name: 'Debug', value: 'DEBUG'}\r\n ]\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logIpAddresses',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Log IP addresses'\r\n }\r\n }\r\n\r\n\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Updating'},\r\n fieldGroup: [\r\n\r\n {\r\n key: 'gitPath',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n label: 'Git executable',\r\n help: 'Set if git is not in your path'\r\n }\r\n },\r\n {\r\n key: 'branch',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Repository branch',\r\n required: true,\r\n help: 'Stay on master. Seriously...'\r\n }\r\n }\r\n ]\r\n },\r\n\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Other'},\r\n fieldGroup: [\r\n {\r\n key: 'keepSearchResultsForDays',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Store results for ...',\r\n addonRight: {\r\n text: 'days'\r\n },\r\n required: true,\r\n help: 'Meta data from searches is stored in the database. When they\\'re deleted links to hydra become invalid.'\r\n }\r\n },\r\n {\r\n key: 'debug',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Enable debugging',\r\n help: \"Only do this if you know what and why you're doing it\"\r\n }\r\n },\r\n {\r\n key: 'runThreaded',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Run threaded server',\r\n help: 'Requires restart'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'startupBrowser',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Open browser on startup'\r\n }\r\n },\r\n {\r\n key: 'shutdownForRestart',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Shutdown to restart',\r\n help: 'When run with a service manager which automatically restarts Hydra enable this to prevent duplicate instances'\r\n }\r\n }\r\n ]\r\n }\r\n ],\r\n\r\n searching: [\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {\r\n label: 'Indexer access'\r\n },\r\n fieldGroup: [\r\n {\r\n key: 'timeout',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Timeout when accessing indexers',\r\n addonRight: {\r\n text: 'seconds'\r\n }\r\n }\r\n },\r\n {\r\n key: 'ignoreTemporarilyDisabled',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Ignore temporarily disabled',\r\n help: \"If enabled access to indexers will never be paused after an error occurred\"\r\n }\r\n },\r\n {\r\n key: 'ignorePassworded',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Ignore passworded releases',\r\n help: \"Not all indexers provide this information\"\r\n }\r\n },\r\n {\r\n key: 'forbiddenWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden words',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: \"Results with any of these words in the title will be ignored\"\r\n }\r\n },\r\n {\r\n key: 'forbiddenRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden regex',\r\n help: 'Must not be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: 'requiredWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required words',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: \"Only results with at least one of these words in the title will be used\"\r\n }\r\n },\r\n {\r\n key: 'requiredRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required regex',\r\n help: 'Must be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: 'applyRestrictions',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Apply word restrictions',\r\n options: [\r\n {name: 'Internal searches', value: 'internal'},\r\n {name: 'API searches', value: 'external'},\r\n {name: 'All searches', value: 'both'}\r\n ],\r\n help: \"For which type of search word restrictions will be applied\"\r\n }\r\n },\r\n {\r\n key: 'forbiddenGroups',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden groups',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: 'Posts from any groups containing any of these words will be ignored'\r\n }\r\n },\r\n {\r\n key: 'forbiddenPosters',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden posters',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: 'Posts from any posters containing any of these words will be ignored'\r\n }\r\n },\r\n {\r\n key: 'maxAge',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Maximum results age',\r\n help: 'Results older than this are ignored. Can be overwritten per search',\r\n addonRight: {\r\n text: 'days'\r\n }\r\n }\r\n },\r\n {\r\n key: 'generate_queries',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Generate queries',\r\n options: [\r\n {label: 'Internal searches', id: 'internal'},\r\n {label: 'API searches', id: 'external'}\r\n ],\r\n help: \"Generate queries for indexers which do not support ID based searches\"\r\n }\r\n },\r\n {\r\n key: 'idFallbackToTitle',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Fallback to title queries',\r\n options: [\r\n {label: 'Internal searches', id: 'internal'},\r\n {label: 'API searches', id: 'external'}\r\n ],\r\n help: \"When no results were found for a query ID search again using the title\"\r\n }\r\n },\r\n {\r\n key: 'userAgent',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'User agent',\r\n required: true\r\n }\r\n }\r\n\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {\r\n label: 'Result processing'\r\n },\r\n fieldGroup: [\r\n {\r\n key: 'htmlParser',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'HTML parser',\r\n options: [\r\n {name: 'Default BS (slower)', value: 'html.parser'},\r\n {name: 'LXML (faster, needs to be installed separately)', value: 'lxml'}\r\n ]\r\n }\r\n },\r\n {\r\n key: 'duplicateSizeThresholdInPercent',\r\n type: 'horizontalPercentInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Duplicate size threshold',\r\n required: true,\r\n addonRight: {\r\n text: '%'\r\n }\r\n\r\n }\r\n },\r\n {\r\n key: 'duplicateAgeThreshold',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Duplicate age threshold',\r\n required: true,\r\n addonRight: {\r\n text: 'hours'\r\n }\r\n }\r\n },\r\n {\r\n key: 'alwaysShowDuplicates',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Always show duplicates',\r\n help: 'Activate to show duplicates in search results by default'\r\n }\r\n },\r\n {\r\n key: 'removeLanguage',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Remove language from newznab titles',\r\n help: 'Some indexers add the language to the result title, preventing proper duplicate detection'\r\n }\r\n },\r\n {\r\n key: 'removeObfuscated',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Remove \"obfuscated\" from nzbgeek titles'\r\n }\r\n },\r\n {\r\n key: 'nzbAccessType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'NZB access type',\r\n options: [\r\n {name: 'Proxy NZBs from indexer', value: 'serve'},\r\n {name: 'Redirect to the indexer', value: 'redirect'}\r\n ],\r\n help: \"How access to NZBs is provided when NZBs are downloaded (by the user or external tools). Redirecting is recommended.\"\r\n }\r\n }\r\n ]\r\n }\r\n ],\r\n\r\n categories: getCategoryFields(),\r\n\r\n downloaders: [\r\n {\r\n type: \"arrayConfig\",\r\n data: {\r\n defaultModel: {\r\n enabled: true\r\n },\r\n entryTemplateUrl: 'downloaderEntry.html',\r\n presets: function () {\r\n return getDownloaderPresets();\r\n },\r\n checkAddingAllowed: function () {\r\n return true;\r\n },\r\n presetsOnly: true,\r\n addNewText: 'Add new downloader',\r\n fieldsFunction: getDownloaderBoxFields,\r\n allowDeleteFunction: function () {\r\n return true;\r\n },\r\n checkBeforeClose: function (scope, model) {\r\n var DownloaderCheckBeforeCloseService = $injector.get(\"DownloaderCheckBeforeCloseService\");\r\n return DownloaderCheckBeforeCloseService.check(scope, model);\r\n },\r\n resetFunction: function (scope) {\r\n scope.options.resetModel();\r\n scope.options.resetModel();\r\n }\r\n\r\n }\r\n }\r\n ],\r\n\r\n\r\n indexers: [\r\n {\r\n type: \"arrayConfig\",\r\n data: {\r\n defaultModel: {\r\n animeCategory: null,\r\n comicCategory: null,\r\n audiobookCategory: null,\r\n magazineCategory: null,\r\n ebookCategory: null,\r\n enabled: true,\r\n categories: [],\r\n downloadLimit: null,\r\n host: null,\r\n apikey: null,\r\n hitLimit: null,\r\n hitLimitResetTime: 0,\r\n timeout: null,\r\n name: null,\r\n showOnSearch: true,\r\n score: 0,\r\n username: null,\r\n password: null,\r\n preselect: true,\r\n type: 'newznab',\r\n accessType: \"both\",\r\n search_ids: undefined, //[\"imdbid\", \"rid\", \"tvdbid\"],\r\n searchTypes: undefined, //[\"tvsearch\", \"movie\"]\r\n backend: 'newznab',\r\n userAgent: null\r\n },\r\n addNewText: 'Add new indexer',\r\n entryTemplateUrl: 'indexerEntry.html',\r\n presets: function (model) {\r\n return getIndexerPresets(model);\r\n },\r\n\r\n checkAddingAllowed: function (existingIndexers, preset) {\r\n if (!preset || !(preset.type == \"anizb\" || preset.type == \"binsearch\" || preset.type == \"nzbindex\" || preset.type == \"nzbclub\")) {\r\n return true;\r\n }\r\n return !_.any(existingIndexers, function (existingEntry) {\r\n return existingEntry.name == preset.name;\r\n });\r\n\r\n },\r\n fieldsFunction: getIndexerBoxFields,\r\n allowDeleteFunction: function (model) {\r\n return true;\r\n },\r\n checkBeforeClose: function (scope, model) {\r\n var IndexerCheckBeforeCloseService = $injector.get(\"IndexerCheckBeforeCloseService\");\r\n return IndexerCheckBeforeCloseService.check(scope, model);\r\n },\r\n resetFunction: function (scope) {\r\n //Then reset the model twice (for some reason when we do it once the search types / ids fields are empty, resetting again fixes that... (wtf))\r\n scope.options.resetModel();\r\n scope.options.resetModel();\r\n }\r\n\r\n }\r\n }\r\n ],\r\n\r\n auth: [\r\n {\r\n key: 'authType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Auth type',\r\n options: [\r\n {name: 'None', value: 'none'},\r\n {name: 'HTTP Basic auth', value: 'basic'},\r\n {name: 'Login form', value: 'form'}\r\n ]\r\n\r\n }\r\n },\r\n {\r\n key: 'restrictSearch',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Restrict searching',\r\n help: 'Restrict access to searching'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n key: 'restrictStats',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Restrict stats',\r\n help: 'Restrict access to stats'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n key: 'restrictAdmin',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Restrict admin',\r\n help: 'Restrict access to admin functions'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n key: 'rememberUsers',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Remember users',\r\n help: 'Remember users with cookie for 14 days'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n type: 'repeatSection',\r\n key: 'users',\r\n model: rootModel.auth,\r\n templateOptions: {\r\n btnText: 'Add new user',\r\n altLegendText: 'Authless',\r\n fields: [\r\n {\r\n key: 'username',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Username',\r\n required: true\r\n }\r\n\r\n },\r\n {\r\n key: 'password',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'password',\r\n label: 'Password',\r\n required: true\r\n }\r\n },\r\n {\r\n key: 'maySeeAdmin',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'May see admin area'\r\n }\r\n },\r\n {\r\n key: 'maySeeStats',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'May see stats'\r\n },\r\n hideExpression: 'model.maySeeAdmin'\r\n }\r\n\r\n ],\r\n defaultModel: {\r\n username: null,\r\n password: null,\r\n maySeeStats: true,\r\n maySeeAdmin: true\r\n }\r\n }\r\n }\r\n ]\r\n };\r\n }\r\n}\r\nConfigFields.$inject = [\"$injector\"];\r\n\r\n\r\nfunction getIndexerPresets(configuredIndexers) {\r\n var presets = [\r\n [\r\n {\r\n name: \"6box\",\r\n host: \"https://6box.me\"\r\n },\r\n {\r\n name: \"6box spotweb\",\r\n host: \"https://6box.me/spotweb\"\r\n },\r\n {\r\n name: \"altHUB\",\r\n host: \"https://api.althub.co.za\"\r\n },\r\n {\r\n name: \"DogNZB\",\r\n host: \"https://api.dognzb.cr\"\r\n },\r\n {\r\n name: \"Drunken Slug\",\r\n host: \"https://api.drunkenslug.com\"\r\n },\r\n {\r\n name: \"LuluNZB\",\r\n host: \"https://lulunzb.com\"\r\n },\r\n {\r\n name: \"miatrix\",\r\n host: \"https://www.miatrix.com\"\r\n },\r\n {\r\n name: \"newz69.keagaming\",\r\n host: \"https://newz69.keagaming.com\"\r\n },\r\n {\r\n name: \"NewzTown\",\r\n host: \"https://newztown.co.za\"\r\n },\r\n {\r\n name: \"NZB Finder\",\r\n host: \"https://nzbfinder.ws\"\r\n },\r\n {\r\n name: \"NZBCat\",\r\n host: \"https://nzb.cat\"\r\n },\r\n {\r\n name: \"nzb.ag\",\r\n host: \"https://nzb.ag\"\r\n },\r\n {\r\n name: \"nzb.is\",\r\n host: \"https://nzb.is\"\r\n },\r\n {\r\n name: \"nzb.su\",\r\n host: \"https://api.nzb.su\"\r\n },\r\n {\r\n name: \"nzb7\",\r\n host: \"https://www.nzb7.com\"\r\n },\r\n {\r\n name: \"NZBGeek\",\r\n host: \"https://api.nzbgeek.info\"\r\n },\r\n {\r\n name: \"NzbNdx\",\r\n host: \"https://www.nzbndx.com\"\r\n },\r\n {\r\n name: \"NzBNooB\",\r\n host: \"https://www.nzbnoob.com\"\r\n },\r\n {\r\n name: \"nzbplanet\",\r\n host: \"https://nzbplanet.net\"\r\n },\r\n {\r\n name: \"NZBs.org\",\r\n host: \"https://nzbs.org\"\r\n },\r\n {\r\n name: \"NZBs.io\",\r\n host: \"https://www.nzbs.io\"\r\n },\r\n {\r\n name: \"Nzeeb\",\r\n host: \"https://www.nzeeb.com\"\r\n },\r\n {\r\n name: \"oznzb\",\r\n host: \"https://api.oznzb.com\"\r\n },\r\n {\r\n name: \"omgwtfnzbs\",\r\n host: \"https://api.omgwtfnzbs.me\"\r\n },\r\n {\r\n name: \"PFMonkey\",\r\n host: \"https://www.pfmonkey.com\"\r\n },\r\n {\r\n name: \"SimplyNZBs\",\r\n host: \"https://simplynzbs.com\"\r\n },\r\n {\r\n name: \"Tabula-Rasa\",\r\n host: \"https://www.tabula-rasa.pw\"\r\n },\r\n {\r\n name: \"Usenet-Crawler\",\r\n host: \"https://www.usenet-crawler.com\"\r\n }\r\n ],\r\n [\r\n {\r\n name: \"Jackett/Cardigann\",\r\n host: \"http://127.0.0.1:9117/torznab/YOURTRACKER\",\r\n search_ids: [],\r\n searchTypes: [],\r\n type: \"jackett\",\r\n accessType: \"internal\"\r\n }\r\n ],\r\n [\r\n {\r\n accessType: \"both\",\r\n categories: [\"anime\"],\r\n downloadLimit: null,\r\n enabled: false,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://anizb.org\",\r\n name: \"anizb\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"anizb\",\r\n username: null\r\n },\r\n {\r\n accessType: \"internal\",\r\n categories: [],\r\n downloadLimit: null,\r\n enabled: true,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://binsearch.info\",\r\n name: \"Binsearch\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"binsearch\",\r\n username: null\r\n },\r\n {\r\n accessType: \"internal\",\r\n categories: [],\r\n downloadLimit: null,\r\n enabled: true,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://www.nzbclub.com\",\r\n name: \"NZBClub\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"nzbclub\",\r\n username: null\r\n\r\n },\r\n {\r\n accessType: \"internal\",\r\n categories: [],\r\n downloadLimit: null,\r\n enabled: true,\r\n generalMinSize: 1,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://nzbindex.com\",\r\n name: \"NZBIndex\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"nzbindex\",\r\n username: null\r\n\r\n }\r\n ]\r\n ];\r\n\r\n\r\n return presets;\r\n}\r\n\r\nfunction getIndexerBoxFields(model, parentModel, isInitial, injector) {\r\n var fieldset = [];\r\n\r\n fieldset.push({\r\n key: 'enabled',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Enabled'\r\n }\r\n });\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'name',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Name',\r\n required: true,\r\n help: 'Used for identification. Changing the name will lose all history and stats!'\r\n },\r\n validators: {\r\n uniqueName: {\r\n expression: function (viewValue) {\r\n if (isInitial || viewValue != model.name) {\r\n return _.pluck(parentModel, \"name\").indexOf(viewValue) == -1;\r\n }\r\n return true;\r\n },\r\n message: '\"Indexer \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\r\n }\r\n }\r\n })\r\n }\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'host',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Host',\r\n required: true,\r\n placeholder: 'http://www.someindexer.com'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'apikey',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'API Key'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n fieldset.push(\r\n {\r\n key: 'score',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Priority',\r\n required: true,\r\n help: 'When duplicate search results are found the result from the indexer with the highest number will be selected'\r\n }\r\n });\r\n\r\n fieldset.push(\r\n {\r\n key: 'timeout',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Timeout',\r\n help: 'Supercedes the general timeout in \"Searching\"'\r\n }\r\n });\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'hitLimit',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'API hit limit',\r\n help: 'Maximum number of API hits since \"API hit reset time\"'\r\n }\r\n },\r\n {\r\n key: 'downloadLimit',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Download limit',\r\n help: 'When # of downloads since \"Hit reset time\" is reached indexer will not be searched.'\r\n }\r\n }\r\n );\r\n fieldset.push(\r\n {\r\n key: 'hitLimitResetTime',\r\n type: 'horizontalInput',\r\n hideExpression: '!model.hitLimit && !model.downloadLimit',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Hit reset time',\r\n help: 'UTC hour of day at which the API hit counter is reset (0==24). Leave empty for a rolling reset counter'\r\n },\r\n validators: {\r\n timeOfDay: {\r\n expression: function ($viewValue, $modelValue) {\r\n var value = $modelValue || $viewValue;\r\n return value >= 0 && value <= 24;\r\n },\r\n message: '$viewValue + \" is not a valid hour of day (0-24)\"'\r\n }\r\n\r\n }\r\n });\r\n }\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'username',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n required: false,\r\n label: 'Username',\r\n help: 'Only needed if indexer requires HTTP auth for API access (rare)'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n );\r\n }\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'password',\r\n type: 'horizontalInput',\r\n hideExpression: '!model.username',\r\n templateOptions: {\r\n type: 'text',\r\n required: false,\r\n label: 'Password',\r\n help: 'Only needed if indexer requires HTTP auth for API access (rare)'\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'userAgent',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n required: false,\r\n label: 'User agent',\r\n help: 'Rarely needed. Will supercede the one in the main searching settings'\r\n }\r\n }\r\n )\r\n }\r\n\r\n\r\n fieldset.push(\r\n {\r\n key: 'preselect',\r\n type: 'horizontalSwitch',\r\n hideExpression: 'model.accessType == \"external\"',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Preselect',\r\n help: 'Preselect this indexer on the search page'\r\n }\r\n }\r\n );\r\n if (model.type != \"jackett\") {\r\n fieldset.push(\r\n {\r\n key: 'accessType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Enable for...',\r\n options: [\r\n {name: 'Internal searches only', value: 'internal'},\r\n {name: 'API searches only', value: 'external'},\r\n {name: 'Internal and API searches', value: 'both'}\r\n ]\r\n }\r\n }\r\n );\r\n }\r\n if (model.type != \"anizb\") {\r\n fieldset.push(\r\n {\r\n key: 'categories',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Enable for...',\r\n help: 'You can decide that this indexer should only be used for certain categories',\r\n options: [\r\n {\r\n id: \"movies\",\r\n label: \"Movies\"\r\n },\r\n {\r\n id: \"movieshd\",\r\n label: \"Movies HD\"\r\n },\r\n {\r\n id: \"moviessd\",\r\n label: \"Movies SD\"\r\n },\r\n {\r\n id: \"tv\",\r\n label: \"TV\"\r\n },\r\n {\r\n id: \"tvhd\",\r\n label: \"TV HD\"\r\n },\r\n {\r\n id: \"tvsd\",\r\n label: \"TV SD\"\r\n },\r\n {\r\n id: \"anime\",\r\n label: \"Anime\"\r\n },\r\n {\r\n id: \"audio\",\r\n label: \"Audio\"\r\n },\r\n {\r\n id: \"flac\",\r\n label: \"Audio FLAC\"\r\n },\r\n {\r\n id: \"mp3\",\r\n label: \"Audio MP3\"\r\n },\r\n {\r\n id: \"audiobook\",\r\n label: \"Audiobook\"\r\n },\r\n {\r\n id: \"console\",\r\n label: \"Console\"\r\n },\r\n {\r\n id: \"pc\",\r\n label: \"PC\"\r\n },\r\n {\r\n id: \"xxx\",\r\n label: \"XXX\"\r\n },\r\n {\r\n id: \"ebook\",\r\n label: \"Ebook\"\r\n },\r\n {\r\n id: \"comic\",\r\n label: \"Comic\"\r\n }],\r\n getPlaceholder: function () {\r\n return \"All categories\";\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'search_ids',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Search IDs',\r\n options: [\r\n {label: 'TVDB', id: 'tvdbid'},\r\n {label: 'TVRage', id: 'rid'},\r\n {label: 'IMDB', id: 'imdbid'},\r\n {label: 'Trakt', id: 'traktid'},\r\n {label: 'TVMaze', id: 'tvmazeid'},\r\n {label: 'TMDB', id: 'tmdbid'}\r\n ],\r\n getPlaceholder: function (model) {\r\n if (angular.isUndefined(model)) {\r\n return \"Unknown\";\r\n }\r\n return \"None\";\r\n }\r\n }\r\n }\r\n );\r\n }\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'searchTypes',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Search types',\r\n options: [\r\n {label: 'Movies', id: 'movie'},\r\n {label: 'TV', id: 'tvsearch'},\r\n {label: 'Ebooks', id: 'book'},\r\n {label: 'Audio', id: 'audio'}\r\n ],\r\n getPlaceholder: function (model) {\r\n if (angular.isUndefined(model)) {\r\n return \"Unknown\";\r\n }\r\n return \"None\";\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n type: 'horizontalCheckCaps',\r\n hideExpression: '!model.host || !model.apikey || !model.name',\r\n templateOptions: {\r\n label: 'Check capabilities',\r\n help: 'Find out what search types the indexer supports. Done automatically for new indexers.'\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'nzbindex') {\r\n fieldset.push(\r\n {\r\n key: 'generalMinSize',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Min size',\r\n help: 'NZBIndex returns a lot of crap with small file sizes. Set this value and all smaller results will be filtered out no matter the category'\r\n }\r\n }\r\n );\r\n }\r\n\r\n return fieldset;\r\n}\r\n\r\n\r\nfunction getDownloaderBoxFields(model, parentModel, isInitial) {\r\n var fieldset = [];\r\n\r\n fieldset = _.union(fieldset, [\r\n {\r\n key: 'enabled',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Enabled'\r\n }\r\n },\r\n {\r\n key: 'name',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Name',\r\n required: true\r\n },\r\n validators: {\r\n uniqueName: {\r\n expression: function (viewValue) {\r\n if (isInitial || viewValue != model.name) {\r\n return _.pluck(parentModel, \"name\").indexOf(viewValue) == -1;\r\n }\r\n return true;\r\n },\r\n message: '\"Downloader \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\r\n }\r\n }\r\n\r\n }]);\r\n\r\n if (model.type == \"nzbget\") {\r\n fieldset = _.union(fieldset, [{\r\n key: 'host',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Host',\r\n required: true\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n\r\n },\r\n {\r\n key: 'port',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Port',\r\n placeholder: '5050',\r\n required: true\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }, {\r\n key: 'ssl',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Use SSL'\r\n }\r\n }]);\r\n } else if (model.type == \"sabnzbd\") {\r\n fieldset.push({\r\n key: 'url',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'URL',\r\n required: true\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n });\r\n }\r\n fieldset = _.union(fieldset, [\r\n {\r\n key: 'username',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Username',\r\n help: model.type == \"nzbget\" ? 'Only alphanumeric usernames are guaranteed to work' : \"\"\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n },\r\n {\r\n key: 'password',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'password',\r\n label: 'Password',\r\n help: model.type == \"nzbget\" ? 'See username' : \"\"\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n ]);\r\n\r\n\r\n if (model.type == \"sabnzbd\") {\r\n fieldset.push({\r\n key: 'apikey',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'API Key'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n })\r\n }\r\n\r\n fieldset = _.union(fieldset, [\r\n {\r\n key: 'defaultCategory',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Default category',\r\n help: 'When adding NZBs this category will be used instead of asking for the category. Write \"No category\" to let the downloader decide.',\r\n placeholder: 'Ask when downloading'\r\n }\r\n },\r\n {\r\n key: 'nzbaccesstype',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'NZB access type',\r\n options: [\r\n {name: 'Proxy NZBs from indexer', value: 'serve'},\r\n {name: 'Redirect to the indexer', value: 'redirect'}\r\n ],\r\n help: \"How external access to NZBs is provided. Redirecting is recommended.\"\r\n }\r\n },\r\n {\r\n key: 'nzbAddingType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'NZB adding type',\r\n options: [\r\n {name: 'Send link', value: 'link'},\r\n {name: 'Upload NZB', value: 'nzb'}\r\n ],\r\n help: \"How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data\"\r\n }\r\n },\r\n {\r\n key: 'iconCssClass',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Icon CSS class',\r\n help: 'Copy an icon name from http://fontawesome.io/examples/ (e.g. \"film\")',\r\n placeholder: 'Default'\r\n }\r\n }\r\n ]);\r\n\r\n return fieldset;\r\n}\r\n\r\nfunction getDownloaderPresets() {\r\n return [[\r\n {\r\n host: \"127.0.0.1\",\r\n name: \"NZBGet\",\r\n password: \"tegbzn6789x\",\r\n port: 6789,\r\n ssl: false,\r\n type: \"nzbget\",\r\n username: \"nzbgetx\",\r\n nzbAddingType: \"link\",\r\n nzbaccesstype: \"redirect\",\r\n iconCssClass: \"\",\r\n downloadType: \"nzb\"\r\n },\r\n {\r\n url: \"http://localhost:8086\",\r\n type: \"sabnzbd\",\r\n name: \"SABnzbd\",\r\n nzbAddingType: \"link\",\r\n nzbaccesstype: \"redirect\",\r\n iconCssClass: \"\",\r\n downloadType: \"nzb\",\r\n username: null,\r\n password: null\r\n }\r\n ]];\r\n}\r\n\r\n\r\nfunction handleConnectionCheckFail(ModalService, data, model, whatFailed, deferred) {\r\n var message;\r\n var yesText;\r\n if (data.checked) {\r\n message = \"The connection to the \" + whatFailed + \" failed: \" + data.message + \"
Do you want to add it anyway?\";\r\n yesText = \"I know what I'm doing\";\r\n } else {\r\n message = \"The connection to the \" + whatFailed + \" could not be tested, sorry\";\r\n yesText = \"I'll risk it\";\r\n }\r\n ModalService.open(\"Connection check failed\", message, {\r\n yes: {\r\n onYes: function () {\r\n deferred.resolve();\r\n },\r\n text: yesText\r\n },\r\n no: {\r\n onNo: function () {\r\n model.enabled = false;\r\n deferred.resolve();\r\n },\r\n text: \"Add it, but disabled\"\r\n },\r\n cancel: {\r\n onCancel: function () {\r\n deferred.reject();\r\n },\r\n text: \"Aahh, let me try again\"\r\n }\r\n });\r\n\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('IndexerCheckBeforeCloseService', IndexerCheckBeforeCloseService);\r\n\r\nfunction IndexerCheckBeforeCloseService($q, ModalService, ConfigBoxService, blockUI, growl) {\r\n\r\n return {\r\n check: checkBeforeClose\r\n };\r\n\r\n function checkBeforeClose(scope, model) {\r\n var deferred = $q.defer();\r\n if (!scope.needsConnectionTest) {\r\n checkCaps(scope, model).then(function () {\r\n deferred.resolve();\r\n }, function () {\r\n deferred.reject();\r\n });\r\n } else {\r\n blockUI.start(\"Testing connection...\");\r\n scope.spinnerActive = true;\r\n var url = \"internalapi/test_newznab\";\r\n var settings = {host: model.host, apikey: model.apikey};\r\n if (angular.isDefined(model.username)) {\r\n settings[\"username\"] = model.username;\r\n settings[\"password\"] = model.password;\r\n }\r\n ConfigBoxService.checkConnection(url, JSON.stringify(settings)).then(function () {\r\n checkCaps(scope, model).then(function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n growl.info(\"Connection to the indexer tested successfully\");\r\n deferred.resolve();\r\n }, function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n deferred.reject();\r\n });\r\n },\r\n function (data) {\r\n blockUI.reset();\r\n handleConnectionCheckFail(ModalService, data, model, \"indexer\", deferred);\r\n }).finally(function () {\r\n scope.spinnerActive = false;\r\n blockUI.reset();\r\n });\r\n }\r\n return deferred.promise;\r\n\r\n }\r\n\r\n function checkCaps(scope, model) {\r\n var deferred = $q.defer();\r\n var url = \"internalapi/test_caps\";\r\n var settings = {indexer: model.name, apikey: model.apikey, host: model.host};\r\n if (angular.isDefined(model.username)) {\r\n settings[\"username\"] = model.username;\r\n settings[\"password\"] = model.password;\r\n }\r\n if (angular.isUndefined(model.search_ids) || angular.isUndefined(model.searchTypes)) {\r\n\r\n blockUI.start(\"New indexer found. Testing its capabilities. This may take a bit...\");\r\n ConfigBoxService.checkCaps(url, JSON.stringify(settings), model).then(\r\n function (data, model) {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n growl.info(\"Successfully tested capabilites of indexer\");\r\n deferred.resolve();\r\n },\r\n function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n model.search_ids = [];\r\n model.searchTypes = [];\r\n ModalService.open(\"Error testing capabilities\", \"The capabilities of the indexer could not be checked. The indexer won't be used for ID based searches (IMDB, TVDB, etc.). You may repeat the check manually at any time.\");\r\n deferred.resolve();\r\n }).finally(\r\n function () {\r\n scope.spinnerActive = false;\r\n })\r\n } else {\r\n deferred.resolve();\r\n }\r\n return deferred.promise;\r\n\r\n }\r\n}\r\nIndexerCheckBeforeCloseService.$inject = [\"$q\", \"ModalService\", \"ConfigBoxService\", \"blockUI\", \"growl\"];\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('DownloaderCheckBeforeCloseService', DownloaderCheckBeforeCloseService);\r\n\r\nfunction DownloaderCheckBeforeCloseService($q, ConfigBoxService, growl, ModalService, blockUI) {\r\n\r\n return {\r\n check: checkBeforeClose\r\n };\r\n\r\n function checkBeforeClose(scope, model) {\r\n var deferred = $q.defer();\r\n if (!scope.isInitial && !scope.needsConnectionTest) {\r\n deferred.resolve();\r\n } else {\r\n scope.spinnerActive = true;\r\n blockUI.start(\"Testing connection...\");\r\n var url = \"internalapi/test_downloader\";\r\n ConfigBoxService.checkConnection(url, JSON.stringify(model)).then(function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n growl.info(\"Connection to the downloader tested successfully\");\r\n deferred.resolve();\r\n },\r\n function (data) {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n handleConnectionCheckFail(ModalService, data, model, \"downloader\", deferred);\r\n }).finally(function () {\r\n scope.spinnerActive = false;\r\n blockUI.reset();\r\n });\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n}\r\nDownloaderCheckBeforeCloseService.$inject = [\"$q\", \"ConfigBoxService\", \"growl\", \"ModalService\", \"blockUI\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('ConfigModel', function () {\r\n return {};\r\n });\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('ConfigWatcher', function () {\r\n var $scope;\r\n\r\n return {\r\n watch: watch\r\n };\r\n\r\n function watch(scope) {\r\n $scope = scope;\r\n $scope.$watchGroup([\"config.main.host\"], function () {\r\n }, true);\r\n }\r\n });\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('ConfigController', ConfigController);\r\n\r\nfunction ConfigController($scope, $http, activeTab, ConfigService, config, DownloaderCategoriesService, ConfigFields, ConfigModel, ModalService, RestartService, $state, growl) {\r\n $scope.config = config;\r\n $scope.submit = submit;\r\n $scope.activeTab = activeTab;\r\n\r\n $scope.restartRequired = false;\r\n $scope.ignoreSaveNeeded = false;\r\n\r\n ConfigFields.setRestartWatcher(function () {\r\n $scope.restartRequired = true;\r\n });\r\n \r\n\r\n function submit() {\r\n if ($scope.form.$valid) {\r\n \r\n ConfigService.set($scope.config);\r\n $scope.form.$setPristine();\r\n DownloaderCategoriesService.invalidate();\r\n if ($scope.restartRequired) {\r\n ModalService.open(\"Restart required\", \"The changes you have made may require a restart to be effective.
Do you want to restart now?\", {\r\n yes: {\r\n onYes: function () {\r\n RestartService.restart();\r\n }\r\n },\r\n no: {\r\n onNo: function () {\r\n $scope.restartRequired = false;\r\n }\r\n }\r\n });\r\n }\r\n } else {\r\n growl.error(\"Config invalid. Please check your settings.\");\r\n \r\n //Ridiculously hacky way to make the error messages appear\r\n try {\r\n if (angular.isDefined(form.$error.required)) {\r\n _.each(form.$error.required, function (item) {\r\n if (angular.isDefined(item.$error.required)) {\r\n _.each(item.$error.required, function (item2) {\r\n item2.$setTouched();\r\n });\r\n } \r\n });\r\n }\r\n angular.forEach($scope.form.$error.required, function (field) {\r\n field.$setTouched();\r\n });\r\n } catch(err) {\r\n //\r\n }\r\n \r\n }\r\n }\r\n\r\n ConfigModel = config;\r\n\r\n $scope.fields = ConfigFields.getFields($scope.config);\r\n \r\n $scope.allTabs = [\r\n {\r\n active: false,\r\n state: 'root.config.main',\r\n name: 'Main',\r\n model: ConfigModel.main,\r\n fields: $scope.fields.main\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.auth',\r\n name: 'Authorization',\r\n model: ConfigModel.auth,\r\n fields: $scope.fields.auth\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.searching',\r\n name: 'Searching',\r\n model: ConfigModel.searching,\r\n fields: $scope.fields.searching\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.categories',\r\n name: 'Categories',\r\n model: ConfigModel.categories,\r\n fields: $scope.fields.categories\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.downloader',\r\n name: 'Downloaders',\r\n model: ConfigModel.downloaders,\r\n fields: $scope.fields.downloaders\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.indexers',\r\n name: 'Indexers',\r\n model: ConfigModel.indexers,\r\n fields: $scope.fields.indexers\r\n }\r\n ];\r\n\r\n $scope.isSavingNeeded = function () {\r\n return $scope.form.$dirty && $scope.form.$valid && !$scope.ignoreSaveNeeded;\r\n };\r\n\r\n $scope.goToConfigState = function (index) {\r\n $state.go($scope.allTabs[index].state, {activeTab:index}, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.help = function() {\r\n $http.get(\"internalapi/gethelp\", {params: {id: $scope.activeTab.name}}).then(function(result) {\r\n var html = '' + result.data + \"\";\r\n ModalService.open($scope.activeTab.name + \" - Help\", html, {}, \"lg\");\r\n },\r\n function() {\r\n growl.error(\"Error while loading help\")\r\n })\r\n };\r\n\r\n $scope.$on('$stateChangeStart',\r\n function (event, toState, toParams, fromState, fromParams) {\r\n if ($scope.isSavingNeeded()) {\r\n event.preventDefault();\r\n ModalService.open(\"Unsaved changed\", \"Do you want to save before leaving?\", {\r\n yes: {\r\n onYes: function() {\r\n $scope.submit();\r\n $state.go(toState);\r\n },\r\n text: \"Yes\"\r\n },\r\n no: {\r\n onNo: function () {\r\n $scope.ignoreSaveNeeded = true;\r\n $scope.ctrl.options.resetModel();\r\n $state.go(toState);\r\n },\r\n text: \"No\"\r\n },\r\n cancel: {\r\n onCancel: function () {\r\n event.preventDefault();\r\n },\r\n text: \"Cancel\"\r\n }\r\n });\r\n } \r\n })\r\n}\r\nConfigController.$inject = [\"$scope\", \"$http\", \"activeTab\", \"ConfigService\", \"config\", \"DownloaderCategoriesService\", \"ConfigFields\", \"ConfigModel\", \"ModalService\", \"RestartService\", \"$state\", \"growl\"];\r\n\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('CategoriesService', CategoriesService);\r\n\r\nfunction CategoriesService(ConfigService) {\r\n\r\n return {\r\n getByName: getByName,\r\n getAll: getAll,\r\n getDefault: getDefault\r\n };\r\n\r\n\r\n function getByName(name) {\r\n for (var category in ConfigService.getSafe().categories) {\r\n category = ConfigService.getSafe().categories[category];\r\n if (category.name == name || category.pretty == name) {\r\n return category;\r\n }\r\n }\r\n }\r\n \r\n function getAll() {\r\n return ConfigService.getSafe().categories;\r\n }\r\n \r\n function getDefault() {\r\n return getAll()[1];\r\n }\r\n\r\n}\r\nCategoriesService.$inject = [\"ConfigService\"];","angular\r\n .module('nzbhydraApp')\r\n .factory('BackupService', BackupService);\r\n\r\nfunction BackupService($http) {\r\n\r\n return {\r\n getBackupsList: getBackupsList,\r\n restoreFromFile: restoreFromFile\r\n };\r\n \r\n\r\n function getBackupsList() {\r\n return $http.get('internalapi/getbackups').then(function (data) {\r\n return data.data.backups;\r\n });\r\n }\r\n\r\n function restoreFromFile(filename) {\r\n return $http.get('internalapi/restorefrombackupfile', {params:{filename: filename}}).then(function (response) {\r\n return response;\r\n });\r\n }\r\n\r\n}\r\nBackupService.$inject = [\"$http\"];"],"sourceRoot":"/source/"} \ No newline at end of file +{"version":3,"sources":["nzbhydra.js","directives/updates.js","directives/title-row.js","directives/title-group.js","directives/tab-or-chart.js","directives/search-result.js","directives/search-result-non-title-columns.js","directives/on-finish-render.js","directives/log.js","directives/keep-focus.js","directives/indexer-input.js","directives/focus-on.js","directives/duplicate-group.js","directives/download-nzbs-button.js","directives/dataTableDirectives.js","directives/connection-test.js","directives/cfg-form-entry.js","directives/backup.js","directives/addable-nzbs.js","directives/addable-nzb.js","update-service.js","update-footer-controller.js","system-controller.js","stats-service.js","stats-controller.js","search-service.js","search-results-controller.js","search-history-service.js","search-history-controller.js","search-controller.js","restart-service.js","nzbhydra-control-service.js","nzb-download-service.js","modal.js","modal-service.js","login-controller.js","indexer-statuses-controller.js","index-controller.js","hydra-auth-service.js","header-controller.js","generic-error-handler.js","formly-config.js","filters.js","downloader-categories-service.js","download-history-controller.js","config-service.js","config-fields-service.js","config-controller.js","categories-service.js","backup-service.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACruBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACbA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5RA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACvJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjdA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxgEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"nzbhydra.js","sourcesContent":["var nzbhydraapp = angular.module('nzbhydraApp', ['angular-loading-bar', 'cgBusy', 'ui.bootstrap', 'ipCookie', 'angular-growl', 'angular.filter', 'filters', 'ui.router', 'blockUI', 'mgcrea.ngStrap', 'angularUtils.directives.dirPagination', 'nvd3', 'formly', 'formlyBootstrap', 'frapontillo.bootstrap-switch', 'ui.select', 'ngSanitize', 'checklist-model', 'ngAria', 'ngMessages', 'ui.router.title', 'LocalStorageModule', 'angular.filter', 'ngFileUpload', 'ngCookies']);\r\n\r\nangular.module('nzbhydraApp').config([\"$stateProvider\", \"$urlRouterProvider\", \"$locationProvider\", \"blockUIConfig\", \"$urlMatcherFactoryProvider\", \"localStorageServiceProvider\", \"bootstrapped\", function ($stateProvider, $urlRouterProvider, $locationProvider, blockUIConfig, $urlMatcherFactoryProvider, localStorageServiceProvider, bootstrapped) {\r\n\r\n blockUIConfig.autoBlock = false;\r\n $urlMatcherFactoryProvider.strictMode(false);\r\n\r\n $urlRouterProvider.otherwise(\"/\");\r\n\r\n\r\n $stateProvider\r\n .state('root', {\r\n url: '',\r\n abstract: true,\r\n resolve: {\r\n //loginRequired: loginRequired\r\n },\r\n views: {\r\n 'header': {\r\n templateUrl: 'static/html/states/header.html',\r\n controller: 'HeaderController'\r\n },\r\n 'footer': {\r\n templateUrl: 'footer.html'\r\n }\r\n }\r\n })\r\n .state(\"root.config\", {\r\n url: \"/config\",\r\n views: {},\r\n abstract: true\r\n })\r\n .state(\"root.config.main\", {\r\n url: \"/main\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n controllerAs: 'ctrl',\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 0;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Main)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.auth\", {\r\n url: \"/auth\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 1;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Auth)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.searching\", {\r\n url: \"/searching\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 2;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Searching)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.categories\", {\r\n url: \"/categories\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 3;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Categories)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.downloader\", {\r\n url: \"/downloader\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 4;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Downloader)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.config.indexers\", {\r\n url: \"/indexers\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/config.html\",\r\n controller: \"ConfigController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.get();\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 5;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Config (Indexers)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats\", {\r\n url: \"/stats\",\r\n abstract: true,\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/stats.html\",\r\n controller: [\"$scope\", \"$state\", function ($scope, $state) {\r\n $scope.$state = $state;\r\n }],\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats\"\r\n }]\r\n }\r\n\r\n }\r\n }\r\n })\r\n .state(\"root.stats.main\", {\r\n url: \"/stats\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: \"static/html/states/main-stats.html\",\r\n controller: \"StatsController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats.indexers\", {\r\n url: \"/indexers\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: \"static/html/states/indexer-statuses.html\",\r\n controller: IndexerStatusesController,\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n statuses: [\"$http\", function ($http) {\r\n return $http.get(\"internalapi/getindexerstatuses\").success(function (response) {\r\n return response.indexerStatuses;\r\n });\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats (Indexers)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats.searches\", {\r\n url: \"/searches\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: \"static/html/states/search-history.html\",\r\n controller: SearchHistoryController,\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n history: ['loginRequired', 'SearchHistoryService', function (loginRequired, SearchHistoryService) {\r\n return SearchHistoryService.getSearchHistory();\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats (Searches)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.stats.downloads\", {\r\n url: \"/downloads\",\r\n views: {\r\n 'stats@root.stats': {\r\n templateUrl: 'static/html/states/download-history.html',\r\n controller: DownloadHistoryController,\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\r\n }],\r\n downloads: [\"StatsService\", function (StatsService) {\r\n return StatsService.getDownloadHistory();\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Stats (Downloads)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system\", {\r\n url: \"/system\",\r\n views: {},\r\n abstract: true\r\n })\r\n .state(\"root.system.control\", {\r\n url: \"/control\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n askAdmin: ['loginRequired', '$http', function (loginRequired, $http) {\r\n return $http.get(\"internalapi/askadmin\");\r\n }],\r\n activeTab: [function () {\r\n return 0;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.updates\", {\r\n url: \"/updates\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 1;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Updates)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.log\", {\r\n url: \"/log\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 2;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Log)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.backup\", {\r\n url: \"/backup\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 3;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Backup)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.bugreport\", {\r\n url: \"/bugreport\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 4;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (Bug report)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.system.about\", {\r\n url: \"/about\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/system.html\",\r\n controller: \"SystemController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n activeTab: [function () {\r\n return 5;\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"System (About)\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n\r\n .state(\"root.search\", {\r\n url: \"/?category&query&imdbid&tvdbid&title&season&episode&minsize&maxsize&minage&maxage&offsets&rid&mode&tmdbid&indexers\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/search.html\",\r\n controller: \"SearchController\",\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\r\n }],\r\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\r\n return ConfigService.getSafe();\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Search\";\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.search.results\", {\r\n views: {\r\n 'results@root.search': {\r\n templateUrl: \"static/html/states/search-results.html\",\r\n controller: \"SearchResultsController\",\r\n controllerAs: \"srController\",\r\n options: {\r\n inherit: true\r\n },\r\n resolve: {\r\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\r\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\r\n }],\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n var title = \"Search results\";\r\n var details;\r\n if ($stateParams.title) {\r\n details = $stateParams.title;\r\n } else if ($stateParams.query) {\r\n details = $stateParams.query;\r\n }\r\n if (details) {\r\n title += \" (\" + details + \")\";\r\n }\r\n return title;\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n .state(\"root.login\", {\r\n url: \"/login\",\r\n views: {\r\n 'container@': {\r\n templateUrl: \"static/html/states/login.html\",\r\n controller: \"LoginController\",\r\n resolve: {\r\n loginRequired: function () {\r\n return null;\r\n },\r\n $title: [\"$stateParams\", function ($stateParams) {\r\n return \"Login\"\r\n }]\r\n }\r\n }\r\n }\r\n })\r\n ;\r\n\r\n\r\n $locationProvider.html5Mode(true);\r\n\r\n\r\n function loginRequired($q, $timeout, $state, HydraAuthService, type) {\r\n var deferred = $q.defer();\r\n var userInfos = HydraAuthService.getUserInfos();\r\n var allowed = false;\r\n if (type == \"search\") {\r\n allowed = !userInfos.searchRestricted || userInfos.maySeeSearch;\r\n } else if (type == \"stats\") {\r\n allowed = !userInfos.statsRestricted || userInfos.maySeeStats;\r\n } else if (type == \"admin\") {\r\n allowed = !userInfos.adminRestricted || userInfos.maySeeAdmin;\r\n } else {\r\n allowed = true;\r\n }\r\n if (allowed || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n\r\n //Because I don't know for what state the login is required / asked I have a function for each\r\n\r\n function loginRequiredSearch($q, $timeout, $state, HydraAuthService) {\r\n var deferred = $q.defer();\r\n var userInfos = HydraAuthService.getUserInfos();\r\n if (!userInfos.searchRestricted || userInfos.maySeeSearch || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n function loginRequiredStats($q, $timeout, $state, HydraAuthService) {\r\n var deferred = $q.defer();\r\n\r\n var userInfos = HydraAuthService.getUserInfos();\r\n if (!userInfos.statsRestricted || userInfos.maySeeStats || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n function loginRequiredAdmin($q, $timeout, $state, HydraAuthService) {\r\n var deferred = $q.defer();\r\n\r\n var userInfos = HydraAuthService.getUserInfos();\r\n if (!userInfos.statsRestricted || userInfos.maySeeAdmin || userInfos.authType != \"form\") {\r\n deferred.resolve();\r\n } else {\r\n $timeout(function () {\r\n // This code runs after the authentication promise has been rejected.\r\n // Go to the log-in page\r\n $state.go(\"root.login\");\r\n })\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n localStorageServiceProvider\r\n .setPrefix('nzbhydra');\r\n localStorageServiceProvider\r\n .setNotify(true, false);\r\n}]);\r\n\r\n\r\nnzbhydraapp.config([\"paginationTemplateProvider\", function (paginationTemplateProvider) {\r\n paginationTemplateProvider.setPath('static/html/dirPagination.tpl.html');\r\n}]);\r\n\r\nnzbhydraapp.config(['cfpLoadingBarProvider', function (cfpLoadingBarProvider) {\r\n cfpLoadingBarProvider.latencyThreshold = 100;\r\n}]);\r\n\r\nnzbhydraapp.config(['growlProvider', function (growlProvider) {\r\n growlProvider.globalTimeToLive(5000);\r\n growlProvider.globalPosition('bottom-right');\r\n}]);\r\n\r\nnzbhydraapp.directive('ngEnter', function () {\r\n return function (scope, element, attr) {\r\n element.bind(\"keydown keypress\", function (event) {\r\n if (event.which === 13) {\r\n scope.$apply(function () {\r\n scope.$evalAsync(attr.ngEnter);\r\n });\r\n\r\n event.preventDefault();\r\n }\r\n });\r\n };\r\n});\r\n\r\nnzbhydraapp.filter('nzblink', function () {\r\n return function (resultItem) {\r\n var uri = new URI(\"internalapi/getnzb\");\r\n uri.addQuery(\"searchResultId\", resultItem.searchResultId);\r\n return uri.toString();\r\n }\r\n});\r\n\r\nnzbhydraapp.factory('focus', [\"$rootScope\", \"$timeout\", function ($rootScope, $timeout) {\r\n return function (name) {\r\n $timeout(function () {\r\n $rootScope.$broadcast('focusOn', name);\r\n });\r\n }\r\n}]);\r\n\r\nnzbhydraapp.run([\"$rootScope\", function ($rootScope) {\r\n $rootScope.$on('$stateChangeSuccess',\r\n function (event, toState, toParams, fromState, fromParams) {\r\n try {\r\n $rootScope.title = toState.views[Object.keys(toState.views)[0]].resolve.$title[1](toParams);\r\n } catch (e) {\r\n\r\n }\r\n\r\n });\r\n}]);\r\n\r\n\r\nnzbhydraapp.filter('unsafe', [\"$sce\", function ($sce) {\r\n return $sce.trustAsHtml;\r\n}]);\r\n\r\nnzbhydraapp.filter('dereferer', [\"ConfigService\", function (ConfigService) {\r\n return function (url) {\r\n if (ConfigService.getSafe().dereferer) {\r\n return ConfigService.getSafe().dereferer.replace(\"$s\", escape(url));\r\n }\r\n return url;\r\n }\r\n}]);\r\n\r\nnzbhydraapp.config([\"$provide\", function ($provide) {\r\n $provide.decorator(\"$exceptionHandler\", ['$delegate', '$injector', function ($delegate, $injector) {\r\n return function (exception, cause) {\r\n $delegate(exception, cause);\r\n try {\r\n console.log(exception);\r\n var stack = exception.stack.split('\\n').map(function (line) {\r\n return line.trim();\r\n });\r\n stack = stack.join(\"\\n\");\r\n //$injector.get(\"$http\").put(\"internalapi/logerror\", {error: stack, cause: angular.isDefined(cause) ? cause.toString() : \"No known cause\"});\r\n\r\n\r\n } catch (e) {\r\n console.error(\"Unable to log JS exception to server\", e);\r\n }\r\n };\r\n }]);\r\n}]);\r\n\r\n_.mixin({\r\n isNullOrEmpty: function (string) {\r\n return (_.isUndefined(string) || _.isNull(string) || (_.isString(string) && string.length === 0))\r\n }\r\n});\r\n\r\nnzbhydraapp.factory('sessionInjector', [\"$injector\", function ($injector) {\r\n var sessionInjector = {\r\n response: function (response) {\r\n if (response.headers(\"Hydra-MaySeeAdmin\") != null) {\r\n $injector.get(\"HydraAuthService\").setLoggedInByBasic(response.headers(\"Hydra-MaySeeStats\") == \"True\", response.headers(\"Hydra-MaySeeAdmin\") == \"True\", response.headers(\"Hydra-Username\"))\r\n }\r\n\r\n return response;\r\n }\r\n };\r\n return sessionInjector;\r\n}]);\r\n\r\nnzbhydraapp.config(['$httpProvider', function ($httpProvider) {\r\n $httpProvider.interceptors.push('sessionInjector');\r\n}]);\r\n\r\nnzbhydraapp.directive('autoFocus', [\"$timeout\", function ($timeout) {\r\n return {\r\n restrict: 'AC',\r\n link: function (_scope, _element) {\r\n $timeout(function () {\r\n _element[0].focus();\r\n }, 0);\r\n }\r\n };\r\n}]);\r\n\r\n\r\nnzbhydraapp.factory('focus', [\"$timeout\", \"$window\", function ($timeout, $window) {\r\n return function (id) {\r\n // timeout makes sure that it is invoked after any other event has been triggered.\r\n // e.g. click events that need to run before the focus or\r\n // inputs elements that are in a disabled state but are enabled when those events\r\n // are triggered.\r\n $timeout(function () {\r\n var element = $window.document.getElementById(id);\r\n if (element)\r\n element.focus();\r\n });\r\n };\r\n}]);\r\n\r\nnzbhydraapp.directive('eventFocus', [\"focus\", function (focus) {\r\n return function (scope, elem, attr) {\r\n elem.on(attr.eventFocus, function () {\r\n focus(attr.eventFocusId);\r\n });\r\n\r\n // Removes bound events in the element itself\r\n // when the scope is destroyed\r\n scope.$on('$destroy', function () {\r\n elem.off(attr.eventFocus);\r\n });\r\n };\r\n}]);","angular\r\n .module('nzbhydraApp')\r\n .directive('hydraupdates', hydraupdates);\r\n\r\nfunction hydraupdates() {\r\n controller.$inject = [\"$scope\", \"UpdateService\", \"$sce\"];\r\n return {\r\n templateUrl: 'static/html/directives/updates.html',\r\n controller: controller\r\n };\r\n\r\n function controller($scope, UpdateService, $sce) {\r\n\r\n $scope.loadingPromise = UpdateService.getVersions().then(function (data) {\r\n $scope.currentVersion = data.data.currentVersion;\r\n $scope.repVersion = data.data.repVersion;\r\n $scope.updateAvailable = data.data.updateAvailable;\r\n $scope.changelog = data.data.changelog;\r\n });\r\n \r\n UpdateService.getVersionHistory().then(function(data) {\r\n $scope.versionHistory = $sce.trustAsHtml(data.data.versionHistory);\r\n });\r\n\r\n $scope.update = function () {\r\n UpdateService.update();\r\n };\r\n\r\n $scope.showChangelog = function () {\r\n UpdateService.showChanges($scope.changelog);\r\n };\r\n \r\n \r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('titleRow', titleRow);\r\n\r\nfunction titleRow() {\r\n return {\r\n templateUrl: 'static/html/directives/title-row.html',\r\n scope: {\r\n duplicates: \"<\",\r\n selected: \"<\",\r\n rowIndex: \"@\"\r\n },\r\n controller: ['$scope', '$element', '$attrs', titleRowController]\r\n };\r\n\r\n function titleRowController($scope) {\r\n $scope.expanded = false;\r\n console.log(\"Building title row\");\r\n $scope.duplicatesToShow = duplicatesToShow;\r\n function duplicatesToShow() {\r\n if ($scope.expanded && $scope.duplicates.length > 1) {\r\n console.log(\"Showing all duplicates in group\");\r\n return $scope.duplicates;\r\n } else {\r\n console.log(\"Showing first duplicate in group\");\r\n return [$scope.duplicates[0]];\r\n }\r\n }\r\n\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('titleGroup', titleGroup);\r\n\r\nfunction titleGroup() {\r\n return {\r\n templateUrl: 'static/html/directives/title-group.html',\r\n scope: {\r\n titles: \"<\",\r\n selected: \"=\",\r\n rowIndex: \"<\",\r\n doShowDuplicates: \"<\",\r\n internalRowIndex: \"@\"\r\n },\r\n controller: ['$scope', '$element', '$attrs', controller],\r\n multiElement: true\r\n };\r\n\r\n function controller($scope, $element, $attrs) {\r\n $scope.expanded = false;\r\n $scope.titleGroupExpanded = false;\r\n\r\n $scope.$on(\"toggleTitleExpansion\", function (event, args) {\r\n $scope.titleGroupExpanded = args;\r\n event.stopPropagation();\r\n });\r\n\r\n\r\n $scope.titlesToShow = titlesToShow;\r\n function titlesToShow() {\r\n return $scope.titles.slice(1);\r\n }\r\n \r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('tabOrChart', tabOrChart);\r\n\r\nfunction tabOrChart() {\r\n return {\r\n templateUrl: 'static/html/directives/tab-or-chart.html',\r\n transclude: {\r\n \"chartSlot\": \"chart\",\r\n \"tableSlot\": \"table\"\r\n },\r\n restrict: 'E',\r\n replace: true,\r\n scope: {\r\n display: \"@\"\r\n }\r\n\r\n };\r\n\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('searchResult', searchResult);\r\n\r\nfunction searchResult() {\r\n return {\r\n templateUrl: 'static/html/directives/search-result.html',\r\n require: '^titleGroup',\r\n scope: {\r\n titleGroup: \"<\",\r\n showDuplicates: \"<\",\r\n selected: \"<\",\r\n rowIndex: \"<\"\r\n },\r\n controller: ['$scope', '$element', '$attrs', controller],\r\n multiElement: true\r\n };\r\n\r\n function controller($scope, $element, $attrs) {\r\n $scope.titleGroupExpanded = false;\r\n $scope.hashGroupExpanded = {};\r\n\r\n $scope.toggleTitleGroup = function () {\r\n $scope.titleGroupExpanded = !$scope.titleGroupExpanded;\r\n if (!$scope.titleGroupExpanded) {\r\n $scope.hashGroupExpanded[$scope.titleGroup[0][0].hash] = false; //Also collapse the first title's duplicates\r\n }\r\n };\r\n\r\n $scope.groupingRowDuplicatesToShow = groupingRowDuplicatesToShow;\r\n function groupingRowDuplicatesToShow() {\r\n if ($scope.showDuplicates && $scope.titleGroup[0].length > 1 && $scope.hashGroupExpanded[$scope.titleGroup[0][0].hash]) {\r\n return $scope.titleGroup[0].slice(1);\r\n } else {\r\n return [];\r\n }\r\n }\r\n\r\n //
0 && titleGroupExpanded\" class=\"search-results-row\">\r\n $scope.otherTitleRowsToShow = otherTitleRowsToShow;\r\n function otherTitleRowsToShow() {\r\n if ($scope.titleGroup.length > 1 && $scope.titleGroupExpanded) {\r\n return $scope.titleGroup.slice(1);\r\n } else {\r\n return [];\r\n }\r\n }\r\n \r\n $scope.hashGroupDuplicatesToShow = hashGroupDuplicatesToShow;\r\n function hashGroupDuplicatesToShow(hashGroup) {\r\n if ($scope.showDuplicates && $scope.hashGroupExpanded[hashGroup[0].hash]) {\r\n return hashGroup.slice(1);\r\n } else {\r\n return [];\r\n }\r\n }\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('otherColumns', otherColumns);\r\n\r\nfunction otherColumns($http, $templateCache, $compile, $window) {\r\n controller.$inject = [\"$scope\", \"$http\", \"$uibModal\", \"growl\"];\r\n return {\r\n scope: {\r\n result: \"<\"\r\n },\r\n multiElement: true,\r\n\r\n link: function (scope, element, attrs) {\r\n $http.get('static/html/directives/search-result-non-title-columns.html', {cache: $templateCache}).success(function (templateContent) {\r\n element.replaceWith($compile(templateContent)(scope));\r\n });\r\n\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, $uibModal, growl) {\r\n \r\n $scope.showNfo = showNfo;\r\n function showNfo(resultItem) {\r\n if (resultItem.has_nfo == 0) {\r\n return;\r\n }\r\n var uri = new URI(\"internalapi/getnfo\");\r\n uri.addQuery(\"searchresultid\", resultItem.searchResultId);\r\n return $http.get(uri.toString()).then(function (response) {\r\n if (response.data.has_nfo) {\r\n $scope.openModal(\"lg\", response.data.nfo)\r\n } else {\r\n if (!angular.isUndefined(resultItem.message)) {\r\n growl.error(resultItem.message);\r\n } else {\r\n growl.info(\"No NFO available\");\r\n }\r\n }\r\n });\r\n }\r\n\r\n $scope.openModal = openModal;\r\n\r\n function openModal(size, nfo) {\r\n var modalInstance = $uibModal.open({\r\n template: '
',\r\n controller: NfoModalInstanceCtrl,\r\n size: size,\r\n resolve: {\r\n nfo: function () {\r\n return nfo;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then();\r\n }\r\n \r\n $scope.downloadNzb = downloadNzb;\r\n \r\n function downloadNzb(resultItem) {\r\n //href = \"{{ result.link }}\"\r\n $window.location.href = resultItem.link;\r\n }\r\n\r\n $scope.getNfoTooltip = function() {\r\n if ($scope.result.has_nfo == 1) {\r\n return \"Show NFO\"\r\n } else if ($scope.result.has_nfo == 2) {\r\n return \"Try to load NFO (may not be available)\";\r\n } else {\r\n return \"No NFO available\";\r\n }\r\n }\r\n }\r\n}\r\notherColumns.$inject = [\"$http\", \"$templateCache\", \"$compile\", \"$window\"];\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('NfoModalInstanceCtrl', NfoModalInstanceCtrl);\r\n\r\nfunction NfoModalInstanceCtrl($scope, $uibModalInstance, nfo) {\r\n\r\n $scope.nfo = nfo;\r\n\r\n $scope.ok = function () {\r\n $uibModalInstance.close($scope.selected.item);\r\n };\r\n\r\n $scope.cancel = function () {\r\n $uibModalInstance.dismiss();\r\n };\r\n}\r\nNfoModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"nfo\"];","//Can be used in an ng-repeat directive to call a function when the last element was rendered\r\n//We use it to mark the end of sorting / filtering so we can stop blocking the UI\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .directive('onFinishRender', onFinishRender);\r\n\r\nfunction onFinishRender($timeout) {\r\n function linkFunction(scope, element, attr) {\r\n \r\n if (scope.$last === true) {\r\n $timeout(function () {\r\n scope.$evalAsync(attr.onFinishRender);\r\n });\r\n }\r\n }\r\n\r\n return {\r\n link: linkFunction\r\n }\r\n}\r\nonFinishRender.$inject = [\"$timeout\"];","angular\r\n .module('nzbhydraApp')\r\n .directive('hydralog', hydralog);\r\n\r\nfunction hydralog() {\r\n controller.$inject = [\"$scope\", \"$http\", \"$sce\", \"$interval\", \"localStorageService\"];\r\n return {\r\n templateUrl: \"static/html/directives/log.html\",\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, $sce, $interval, localStorageService) {\r\n $scope.tailInterval = null;\r\n $scope.doUpdateLog = localStorageService.get(\"doUpdateLog\") != null ? localStorageService.get(\"doUpdateLog\") : false;\r\n $scope.doTailLog = localStorageService.get(\"doTailLog\") != null ? localStorageService.get(\"doTailLog\") : false;\r\n\r\n\r\n function getAndShowLog() {\r\n return $http.get(\"internalapi/getlogs\").success(function (data) {\r\n $scope.log = $sce.trustAsHtml(data.log);\r\n });\r\n }\r\n\r\n $scope.logPromise = getAndShowLog();\r\n\r\n $scope.scrollToBottom = function () {\r\n document.getElementById(\"logfile\").scrollTop = 10000000;\r\n document.getElementById(\"logfile\").scrollTop = 100001000;\r\n };\r\n\r\n $scope.update = function () {\r\n getAndShowLog();\r\n $scope.scrollToBottom();\r\n };\r\n\r\n function startUpdateLogInterval() {\r\n $scope.tailInterval = $interval(function () {\r\n getAndShowLog();\r\n if ($scope.doTailLog) {\r\n $scope.scrollToBottom();\r\n }\r\n }, 5000);\r\n }\r\n\r\n $scope.toggleUpdate = function() {\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n } else if ($scope.tailInterval != null) {\r\n console.log(\"Cancelling\");\r\n $interval.cancel($scope.tailInterval);\r\n localStorageService.set(\"doTailLog\", false);\r\n $scope.doTailLog = false;\r\n }\r\n localStorageService.set(\"doUpdateLog\", $scope.doUpdateLog);\r\n };\r\n\r\n $scope.toggleTailLog = function () {\r\n localStorageService.set(\"doTailLog\", $scope.doTailLog);\r\n };\r\n\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n }\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp').directive(\"keepFocus\", ['$timeout', function ($timeout) {\r\n /*\r\n Intended use:\r\n \r\n */\r\n return {\r\n restrict: 'A',\r\n require: 'ngModel',\r\n link: function ($scope, $element, attrs, ngModel) {\r\n\r\n ngModel.$parsers.unshift(function (value) {\r\n $timeout(function () {\r\n $element[0].focus();\r\n });\r\n return value;\r\n });\r\n\r\n }\r\n };\r\n}])","angular\r\n .module('nzbhydraApp')\r\n .directive('indexerInput', indexerInput);\r\n\r\nfunction indexerInput() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-input.html',\r\n scope: {\r\n indexer: \"=\",\r\n model: \"=\",\r\n onClick: \"=\"\r\n },\r\n replace: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.isFocused = false;\r\n \r\n $scope.onFocus = function() {\r\n $scope.isFocused = true;\r\n };\r\n\r\n $scope.onBlur = function () {\r\n $scope.isFocused = false; \r\n };\r\n \r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp').directive('focusOn', focusOn);\r\n\r\nfunction focusOn() {\r\n return directive;\r\n function directive(scope, elem, attr) {\r\n scope.$on('focusOn', function (e, name) {\r\n if (name === attr.focusOn) {\r\n elem[0].focus();\r\n }\r\n });\r\n }\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('duplicateGroup', duplicateGroup);\r\n\r\nfunction duplicateGroup() {\r\n titleRowController.$inject = [\"$scope\", \"localStorageService\"];\r\n return {\r\n templateUrl: 'static/html/directives/duplicate-group.html',\r\n scope: {\r\n duplicates: \"<\",\r\n selected: \"=\",\r\n isFirstRow: \"<\",\r\n rowIndex: \"<\",\r\n displayTitleToggle: \"<\",\r\n internalRowIndex: \"@\"\r\n },\r\n controller: titleRowController\r\n };\r\n\r\n function titleRowController($scope, localStorageService) {\r\n $scope.internalRowIndex = Number($scope.internalRowIndex);\r\n $scope.rowIndex = Number($scope.rowIndex);\r\n $scope.titlesExpanded = false;\r\n $scope.duplicatesExpanded = false;\r\n $scope.foo = {\r\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") != null ? localStorageService.get(\"duplicatesDisplayed\") : false\r\n };\r\n $scope.duplicatesToShow = duplicatesToShow;\r\n function duplicatesToShow() {\r\n return $scope.duplicates.slice(1);\r\n }\r\n\r\n $scope.toggleTitleExpansion = function () {\r\n $scope.titlesExpanded = !$scope.titlesExpanded;\r\n $scope.$emit(\"toggleTitleExpansion\", $scope.titlesExpanded);\r\n };\r\n\r\n $scope.toggleDuplicateExpansion = function () {\r\n $scope.duplicatesExpanded = !$scope.duplicatesExpanded;\r\n };\r\n\r\n $scope.$on(\"invertSelection\", function () {\r\n for (var i = 0; i < $scope.duplicates.length; i++) {\r\n if ($scope.duplicatesExpanded) {\r\n invertSelection($scope.selected, $scope.duplicates[i]);\r\n } else {\r\n if (i > 0) {\r\n //Always remove duplicates that aren't displayed\r\n invertSelection($scope.selected, $scope.duplicates[i], true);\r\n } else {\r\n invertSelection($scope.selected, $scope.duplicates[i]);\r\n }\r\n }\r\n }\r\n });\r\n\r\n $scope.$on(\"duplicatesDisplayed\", function (event, args) {\r\n $scope.foo.duplicatesDisplayed = args;\r\n });\r\n\r\n $scope.clickCheckbox = function (event) {\r\n var globalCheckboxIndex = $scope.rowIndex * 1000 + $scope.internalRowIndex * 100 + Number(event.currentTarget.dataset.checkboxIndex);\r\n console.log(globalCheckboxIndex);\r\n $scope.$emit(\"checkboxClicked\", event, globalCheckboxIndex, event.currentTarget.checked);\r\n };\r\n\r\n function isBetween(num, betweena, betweenb) {\r\n return (betweena <= num && num <= betweenb) || (betweena >= num && num >= betweenb);\r\n }\r\n\r\n $scope.$on(\"shiftClick\", function (event, startIndex, endIndex, newValue) {\r\n var globalDuplicateGroupIndex = $scope.rowIndex * 1000 + $scope.internalRowIndex * 100;\r\n if (isBetween(globalDuplicateGroupIndex, startIndex, endIndex)) {\r\n\r\n for (var i = 0; i < $scope.duplicates.length; i++) {\r\n if (isBetween(globalDuplicateGroupIndex + i, startIndex, endIndex)) {\r\n if (i == 0 || $scope.duplicatesExpanded) {\r\n console.log(\"Indirectly clicked row with global index \" + (globalDuplicateGroupIndex + i) + \" setting new checkbox value to \" + newValue);\r\n var index = _.indexOf($scope.selected, $scope.duplicates[i]);\r\n if (index == -1 && newValue) {\r\n console.log(\"Adding to selection\");\r\n $scope.selected.push($scope.duplicates[i]);\r\n } else if (index > -1 && !newValue) {\r\n $scope.selected.splice(index, 1);\r\n console.log(\"Removing from selection\");\r\n }\r\n }\r\n }\r\n }\r\n }\r\n });\r\n\r\n function invertSelection(a, b, dontPush) {\r\n var index = _.indexOf(a, b);\r\n if (index > -1) {\r\n a.splice(index, 1);\r\n } else {\r\n if (!dontPush)\r\n a.push(b);\r\n }\r\n }\r\n }\r\n\r\n\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('downloadNzbsButton', downloadNzbsButton);\r\n\r\nfunction downloadNzbsButton() {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/download-nzbs-button.html',\r\n require: ['^searchResults'],\r\n scope: {\r\n searchResults: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService, growl) {\r\n\r\n $scope.downloaders = NzbDownloadService.getEnabledDownloaders();\r\n\r\n $scope.download = function (downloader) {\r\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length == 0) {\r\n growl.info(\"You should select at least one result...\");\r\n } else {\r\n\r\n var values = _.map($scope.searchResults, function (value) {\r\n return value.searchResultId;\r\n });\r\n\r\n NzbDownloadService.download(downloader, values).then(function (response) {\r\n if (response.data.success) {\r\n growl.info(\"Successfully added \" + response.data.added + \" of \" + response.data.of + \" NZBs\");\r\n } else {\r\n growl.error(\"Error while adding NZBs\");\r\n }\r\n }, function () {\r\n growl.error(\"Error while adding NZBs\");\r\n });\r\n }\r\n }\r\n\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp').directive(\"columnFilterWrapper\", columnFilterWrapper);\r\n\r\nfunction columnFilterWrapper() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: 'static/html/dataTable/columnFilterOuter.html',\r\n transclude: true,\r\n controllerAs: 'columnFilterWrapperCtrl',\r\n scope: true,\r\n bindToController: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n var vm = this;\r\n\r\n vm.open = false;\r\n vm.isActive = false;\r\n\r\n vm.toggle = function () {\r\n vm.open = !vm.open;\r\n if (vm.open) {\r\n $scope.$broadcast(\"opened\");\r\n }\r\n };\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\r\n vm.open = false;\r\n vm.isActive = isActive;\r\n })\r\n }\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"freetextFilter\", freetextFilter);\r\n\r\nfunction freetextFilter() {\r\n controller.$inject = [\"$scope\", \"focus\"];\r\n return {\r\n template: '',\r\n require: \"^columnFilterWrapper\",\r\n controllerAs: 'innerController',\r\n scope: {\r\n column: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, focus) {\r\n $scope.data = {};\r\n\r\n $scope.$on(\"opened\", function () {\r\n focus(\"freetext-filter-input\");\r\n });\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n $scope.$emit(\"filter\", $scope.column, {filter: $scope.data.filter, filtertype: \"freetext\"}, angular.isDefined($scope.data.filter) && $scope.data.filter.length > 0);\r\n }\r\n }\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"checkboxesFilter\", checkboxesFilter);\r\n\r\nfunction checkboxesFilter() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n controllerAs: 'checkboxesFilterController',\r\n scope: {\r\n column: \"@\",\r\n entries: \"<\",\r\n preselect: \"<\",\r\n showInvert: \"<\",\r\n isBoolean: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.selected = {\r\n entries: []\r\n };\r\n\r\n if ($scope.preselect) {\r\n $scope.selected.entries = $scope.entries.slice();\r\n }\r\n\r\n $scope.invert = function () {\r\n $scope.selected.entries = _.difference($scope.entries, $scope.selected.entries);\r\n };\r\n\r\n $scope.apply = function () {\r\n console.log($scope.selected);\r\n var isActive = $scope.selected.entries.length < $scope.entries.length;\r\n $scope.$emit(\"filter\", $scope.column, {filter: _.pluck($scope.selected.entries, \"id\"), filtertype: \"checkboxes\", isBoolean: $scope.isBoolean}, isActive)\r\n }\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"booleanFilter\", booleanFilter);\r\n\r\nfunction booleanFilter() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n controllerAs: 'booleanFilterController',\r\n scope: {\r\n column: \"@\",\r\n options: \"<\",\r\n preselect: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n\r\n function controller($scope) {\r\n $scope.selected = {value: $scope.options[$scope.preselect].value};\r\n\r\n $scope.apply = function () {\r\n console.log($scope.selected);\r\n $scope.$emit(\"filter\", $scope.column, {filter: $scope.selected.value, filtertype: \"boolean\"}, $scope.selected.value != $scope.options[0].value)\r\n }\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"timeFilter\", timeFilter);\r\n\r\nfunction timeFilter() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n scope: {\r\n column: \"@\",\r\n selected: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n $scope.apply = function () {\r\n var isActive = $scope.selected.beforeDate || $scope.selected.afterDate;\r\n $scope.$emit(\"filter\", $scope.column, {filter: {after: $scope.selected.afterDate, before: $scope.selected.beforeDate}, filtertype: \"time\"}, isActive)\r\n }\r\n }\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"columnSortable\", columnSortable);\r\n\r\nfunction columnSortable() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: \"static/html/dataTable/columnSortable.html\",\r\n transclude: true,\r\n scope: {\r\n sortMode: \"@\", //0: no sorting, 1: asc, 2: desc\r\n column: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n if (angular.isUndefined($scope.sortMode)) {\r\n $scope.sortMode = 0;\r\n }\r\n\r\n $scope.$on(\"newSortColumn\", function(event, column) {\r\n if (column != $scope.column) {\r\n $scope.sortMode = 0;\r\n }\r\n });\r\n\r\n $scope.sort = function () {\r\n $scope.sortMode = ($scope.sortMode + 1) % 3;\r\n $scope.$emit(\"sort\", $scope.column, $scope.sortMode)\r\n };\r\n\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('connectionTest', connectionTest);\r\n\r\nfunction connectionTest() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/connection-test.html',\r\n require: ['^type', '^data'],\r\n scope: {\r\n type: \"=\",\r\n id: \"=\",\r\n data: \"=\",\r\n downloader: \"=\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.message = \"\";\r\n console.log($scope);\r\n\r\n var testButton = \"#button-test-connection\";\r\n var testMessage = \"#message-test-connection\";\r\n\r\n function showSuccess() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-danger\");\r\n angular.element(testButton).addClass(\"btn-success\");\r\n }\r\n\r\n function showError() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-success\");\r\n angular.element(testButton).addClass(\"btn-danger\");\r\n }\r\n\r\n $scope.testConnection = function () {\r\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\r\n var myInjector = angular.injector([\"ng\"]);\r\n var $http = myInjector.get(\"$http\");\r\n var url;\r\n var params;\r\n if ($scope.type == \"downloader\") {\r\n url = \"internalapi/test_downloader\";\r\n params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};\r\n if ($scope.downloader == \"sabnzbd\") {\r\n params.apikey = $scope.data.apikey;\r\n params.url = $scope.data.url;\r\n } else {\r\n params.host = $scope.data.host;\r\n params.port = $scope.data.port;\r\n params.ssl = $scope.data.ssl;\r\n }\r\n } else if ($scope.data.type == \"newznab\") {\r\n url = \"internalapi/test_newznab\";\r\n params = {host: $scope.data.host, apikey: $scope.data.apikey};\r\n if (angular.isDefined($scope.data.username)) {\r\n params[\"username\"] = $scope.data.username;\r\n params[\"password\"] = $scope.data.password;\r\n }\r\n }\r\n $http.get(url, {params: params}).success(function (result) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \r\n if (result.result) {\r\n angular.element(testMessage).text(\"\");\r\n showSuccess();\r\n } else {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }\r\n\r\n }).error(function () {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }).finally(function () {\r\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\r\n })\r\n }\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('cfgFormEntry', cfgFormEntry);\r\n\r\nfunction cfgFormEntry() {\r\n return {\r\n templateUrl: 'static/html/directives/cfg-form-entry.html',\r\n require: [\"^title\", \"^cfg\"],\r\n scope: {\r\n title: \"@\",\r\n cfg: \"=\",\r\n help: \"@\",\r\n type: \"@?\",\r\n options: \"=?\"\r\n },\r\n controller: [\"$scope\", \"$element\", \"$attrs\", function ($scope, $element, $attrs) {\r\n $scope.type = angular.isDefined($scope.type) ? $scope.type : 'text';\r\n $scope.options = angular.isDefined($scope.type) ? $scope.$eval($attrs.options) : [];\r\n }]\r\n };\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('hydrabackup', hydrabackup);\r\n\r\nfunction hydrabackup() {\r\n controller.$inject = [\"$scope\", \"BackupService\", \"Upload\", \"RequestsErrorHandler\", \"growl\", \"RestartService\", \"$http\"];\r\n return {\r\n templateUrl: 'static/html/directives/backup.html',\r\n controller: controller\r\n };\r\n\r\n function controller($scope, BackupService, Upload, RequestsErrorHandler, growl, RestartService, $http) {\r\n $scope.refreshBackupList = function () {\r\n BackupService.getBackupsList().then(function (backups) {\r\n $scope.backups = backups;\r\n });\r\n };\r\n\r\n $scope.refreshBackupList();\r\n\r\n $scope.uploadActive = false;\r\n\r\n\r\n $scope.createAndDownloadBackupFile = function() {\r\n\r\n $http({method: 'GET', url: 'internalapi/getbackup', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {\r\n var a = document.createElement('a');\r\n var blob = new Blob([data], {'type': \"application/octet-stream\"});\r\n a.href = URL.createObjectURL(blob);\r\n a.download = \"nzbhydra-backup-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\r\n\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n $scope.refreshBackupList();\r\n }).error(function (data, status, headers, config) {\r\n console.log(\"Error:\" + status);\r\n });\r\n\r\n };\r\n\r\n $scope.uploadBackupFile = function (file, errFiles) {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n console.log(\"Hallo\");\r\n $scope.file = file;\r\n $scope.errFile = errFiles && errFiles[0];\r\n if (file) {\r\n $scope.uploadActive = true;\r\n file.upload = Upload.upload({\r\n url: 'internalapi/restorebackup',\r\n data: {content: file}\r\n });\r\n\r\n file.upload.then(function (response) {\r\n $scope.uploadActive = false;\r\n file.result = response.data;\r\n RestartService.restart(\"Restore successful.\");\r\n\r\n }, function (response) {\r\n $scope.uploadActive = false;\r\n growl.error(response.data)\r\n }, function (evt) {\r\n file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));\r\n file.loaded = Math.floor(evt.loaded / 1024);\r\n file.total = Math.floor(evt.total / 1024);\r\n });\r\n }\r\n });\r\n };\r\n\r\n $scope.restoreFromFile = function(filename) {\r\n BackupService.restoreFromFile(filename).then(function() {\r\n RestartService.restart(\"Restore successful.\");\r\n },\r\n function(response) {\r\n growl.error(response.data);\r\n })\r\n }\r\n\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('addableNzbs', addableNzbs);\r\n\r\nfunction addableNzbs() {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\"];\r\n return {\r\n templateUrl: 'static/html/directives/addable-nzbs.html',\r\n require: ['^searchResultId'],\r\n scope: {\r\n searchResultId: \"<\",\r\n downloadType: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService) {\r\n $scope.downloaders = _.filter(NzbDownloadService.getEnabledDownloaders(), function(downloader) {\r\n if ($scope.downloadType != \"nzb\") {\r\n return downloader.downloadType == $scope.downloadType\r\n }\r\n return true;\r\n });\r\n }\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('addableNzb', addableNzb);\r\n\r\nfunction addableNzb() {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/addable-nzb.html',\r\n scope: {\r\n searchResultId: \"<\",\r\n downloader: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService, growl) {\r\n if ($scope.downloader.iconCssClass) {\r\n $scope.cssClass = \"fa fa-\" + $scope.downloader.iconCssClass.replace(\"fa-\",\"\").replace(\"fa \", \"\"); \r\n } else {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd\" : \"nzbget\";\r\n }\r\n \r\n $scope.add = function () {\r\n $scope.cssClass = \"nzb-spinning\";\r\n NzbDownloadService.download($scope.downloader, [$scope.searchResultId]).then(function (response) {\r\n if (response.data.success) {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-success\" : \"nzbget-success\";\r\n } else {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(\"Unable to add NZB. Make sure the downloader is running and properly configured.\");\r\n }\r\n }, function () {\r\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(\"An unexpected error occurred while trying to contact NZB Hydra or add the NZB.\");\r\n })\r\n };\r\n \r\n \r\n\r\n }\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('UpdateService', UpdateService);\r\n\r\nfunction UpdateService($http, growl, blockUI, RestartService) {\r\n\r\n var currentVersion;\r\n var repVersion;\r\n var updateAvailable;\r\n var changelog;\r\n var versionHistory;\r\n \r\n return {\r\n update: update,\r\n showChanges: showChanges,\r\n getVersions: getVersions,\r\n getChangelog: getChangelog,\r\n getVersionHistory: getVersionHistory\r\n };\r\n \r\n \r\n \r\n function getVersions() {\r\n return $http.get(\"internalapi/get_versions\").then(function (data) {\r\n currentVersion = data.data.currentVersion;\r\n repVersion = data.data.repVersion;\r\n updateAvailable = data.data.updateAvailable;\r\n return data;\r\n });\r\n }\r\n\r\n function getChangelog() {\r\n return $http.get(\"internalapi/get_changelog\", {currentVersion: currentVersion, repVersion: repVersion}).then(function (data) {\r\n changelog = data.data.changelog;\r\n return data;\r\n });\r\n }\r\n \r\n function getVersionHistory() {\r\n return $http.get(\"internalapi/get_version_history\").then(function (data) {\r\n versionHistory = data.data.versionHistory;\r\n return data;\r\n });\r\n }\r\n\r\n function showChanges(changelog) {\r\n\r\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\r\n var $uibModal = myInjector.get(\"$uibModal\");\r\n var params = {\r\n size: \"lg\",\r\n templateUrl: \"static/html/changelog.html\",\r\n resolve: {\r\n changelog: function () {\r\n return changelog;\r\n }\r\n },\r\n controller: function ($scope, $sce, $uibModalInstance, changelog) {\r\n //I fucking hate that untrusted HTML shit\r\n changelog = $sce.trustAsHtml(changelog);\r\n $scope.changelog = changelog;\r\n console.log(changelog);\r\n $scope.ok = function () {\r\n $uibModalInstance.dismiss();\r\n };\r\n }\r\n };\r\n\r\n var modalInstance = $uibModal.open(params);\r\n\r\n modalInstance.result.then();\r\n }\r\n \r\n\r\n function update() {\r\n blockUI.start(\"Updating. Please stand by...\");\r\n $http.get(\"internalapi/update\").then(function (data) {\r\n if (data.data.success) {\r\n RestartService.restart(\"Update complete.\", 15);\r\n } else {\r\n blockUI.reset();\r\n growl.info(\"An error occurred while updating. Please check the logs.\");\r\n }\r\n },\r\n function () {\r\n blockUI.reset();\r\n growl.info(\"An error occurred while updating. Please check the logs.\");\r\n });\r\n }\r\n}\r\nUpdateService.$inject = [\"$http\", \"growl\", \"blockUI\", \"RestartService\"];\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('UpdateFooterController', UpdateFooterController);\r\n\r\nfunction UpdateFooterController($scope, UpdateService, HydraAuthService) {\r\n\r\n $scope.updateAvailable = false;\r\n $scope.checked = false;\r\n\r\n $scope.mayUpdate = HydraAuthService.getUserInfos().maySeeAdmin;\r\n\r\n $scope.$on(\"user:loggedIn\", function () {\r\n if (HydraAuthService.getUserInfos().maySeeAdmin && !$scope.checked) {\r\n retrieveUpdateInfos();\r\n }\r\n });\r\n\r\n\r\n if ($scope.mayUpdate) {\r\n retrieveUpdateInfos();\r\n }\r\n\r\n function retrieveUpdateInfos() {\r\n $scope.checked = true;\r\n UpdateService.getVersions().then(function (data) {\r\n $scope.currentVersion = data.data.currentVersion;\r\n $scope.repVersion = data.data.repVersion;\r\n $scope.updateAvailable = data.data.updateAvailable;\r\n $scope.changelog = data.data.changelog;\r\n });\r\n }\r\n\r\n\r\n $scope.update = function () {\r\n UpdateService.update();\r\n };\r\n\r\n $scope.showChangelog = function () {\r\n UpdateService.showChanges($scope.changelog);\r\n }\r\n\r\n}\r\nUpdateFooterController.$inject = [\"$scope\", \"UpdateService\", \"HydraAuthService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('SystemController', SystemController);\r\n\r\nfunction SystemController($scope, $state, activeTab, $http, growl, RestartService, ModalService, UpdateService, NzbHydraControlService) {\r\n\r\n $scope.activeTab = activeTab;\r\n\r\n $scope.shutdown = function () {\r\n NzbHydraControlService.shutdown().then(function () {\r\n growl.info(\"Shutdown initiated. Cya!\");\r\n },\r\n function () {\r\n growl.info(\"Unable to send shutdown command.\");\r\n })\r\n };\r\n\r\n $scope.restart = function () {\r\n RestartService.restart();\r\n };\r\n\r\n $scope.deleteLogAndDatabase = function () {\r\n ModalService.open(\"Delete log and db\", \"Are you absolutely sure you want to delete your database and log files? Hydra will restart to do that.\", {\r\n yes: {\r\n onYes: function () {\r\n NzbHydraControlService.deleteLogAndDb();\r\n RestartService.countdown();\r\n },\r\n text: \"Yes, delete log and database\"\r\n },\r\n no: {\r\n onCancel: function () {\r\n\r\n },\r\n text: \"Nah\"\r\n }\r\n });\r\n };\r\n\r\n $scope.forceUpdate = function() {\r\n UpdateService.update()\r\n };\r\n \r\n\r\n $scope.allTabs = [\r\n {\r\n active: false,\r\n state: 'root.system.control',\r\n name: \"Control\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.updates',\r\n name: \"Updates\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.log',\r\n name: \"Log\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.backup',\r\n name: \"Backup\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.bugreport',\r\n name: \"Bugreport\"\r\n },\r\n {\r\n active: false,\r\n state: 'root.system.about',\r\n name: \"About\"\r\n }\r\n ];\r\n\r\n\r\n $scope.goToSystemState = function (index) {\r\n $state.go($scope.allTabs[index].state, {activeTab: index}, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.downloadDebuggingInfos = function() {\r\n $http({method: 'GET', url: 'internalapi/getdebugginginfos', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {\r\n var a = document.createElement('a');\r\n var blob = new Blob([data], {'type': \"application/octet-stream\"});\r\n a.href = URL.createObjectURL(blob);\r\n var filename = \"nzbhydra-debuginfo-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\r\n a.download = filename;\r\n \r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n }).error(function (data, status, headers, config) {\r\n console.log(\"Error:\" + status);\r\n });\r\n }\r\n \r\n}\r\nSystemController.$inject = [\"$scope\", \"$state\", \"activeTab\", \"$http\", \"growl\", \"RestartService\", \"ModalService\", \"UpdateService\", \"NzbHydraControlService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('StatsService', StatsService);\r\n\r\nfunction StatsService($http) {\r\n\r\n return {\r\n get: getStats,\r\n getDownloadHistory: getDownloadHistory\r\n };\r\n\r\n function getStats(after, before) {\r\n return $http.get(\"internalapi/getstats\", {params: {after:after, before:before}}).success(function (response) {\r\n return response.data;\r\n });\r\n }\r\n\r\n function getDownloadHistory(pageNumber, limit, filterModel, sortModel) {\r\n var params = {page: pageNumber, limit: limit, filterModel: filterModel};\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n }\r\n return $http.post(\"internalapi/getnzbdownloads\", params).success(function (response) {\r\n return {\r\n nzbDownloads: response.nzbDownloads,\r\n totalDownloads: response.totalDownloads\r\n };\r\n \r\n });\r\n }\r\n\r\n}\r\nStatsService.$inject = [\"$http\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('StatsController', StatsController);\r\n\r\nfunction StatsController($scope, $filter, StatsService, blockUI) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n var initializingAfter = true;\r\n var initializingBefore = true;\r\n $scope.afterDate = moment().subtract(30, \"days\").toDate();\r\n $scope.beforeDate = moment().toDate();\r\n updateStats();\r\n\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n function updateStats() {\r\n blockUI.start(\"Updating stats...\");\r\n var after = $scope.afterDate != null ? Math.floor($scope.afterDate.getTime() / 1000) : null;\r\n var before = $scope.beforeDate != null ? Math.floor($scope.beforeDate.getTime() / 1000) : null;\r\n StatsService.get(after, before).then(function(stats) {\r\n $scope.setStats(stats);\r\n });\r\n\r\n blockUI.reset();\r\n }\r\n\r\n $scope.$watch('beforeDate', function () {\r\n if (initializingBefore) {\r\n initializingBefore = false;\r\n } else {\r\n updateStats();\r\n }\r\n });\r\n\r\n\r\n $scope.$watch('afterDate', function () {\r\n if (initializingAfter) {\r\n initializingAfter = false;\r\n } else {\r\n updateStats();\r\n }\r\n });\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n updateStats();\r\n }\r\n };\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n\r\n $scope.setStats = function (stats) {\r\n stats = stats.data;\r\n\r\n $scope.nzbDownloads = null;\r\n $scope.avgResponseTimes = stats.avgResponseTimes;\r\n $scope.avgIndexerSearchResultsShares = stats.avgIndexerSearchResultsShares;\r\n $scope.avgIndexerAccessSuccesses = stats.avgIndexerAccessSuccesses;\r\n $scope.indexerDownloadShares = stats.indexerDownloadShares;\r\n $scope.downloadsPerHourOfDay = stats.timeBasedDownloadStats.perHourOfDay;\r\n $scope.downloadsPerDayOfWeek = stats.timeBasedDownloadStats.perDayOfWeek;\r\n $scope.searchesPerHourOfDay = stats.timeBasedSearchStats.perHourOfDay;\r\n $scope.searchesPerDayOfWeek = stats.timeBasedSearchStats.perDayOfWeek;\r\n\r\n\r\n var numIndexers = $scope.avgResponseTimes.length;\r\n\r\n $scope.avgResponseTimesChart = getChart(\"multiBarHorizontalChart\", $scope.avgResponseTimes, \"name\", \"avgResponseTime\", \"\", \"Response time\");\r\n $scope.avgResponseTimesChart.options.chart.margin.left = 100;\r\n $scope.avgResponseTimesChart.options.chart.yAxis.rotateLabels = -30;\r\n var avgResponseTimesChartHeight = Math.max(numIndexers * 30, 350);\r\n $scope.avgResponseTimesChart.options.chart.height = avgResponseTimesChartHeight;\r\n\r\n $scope.resultsSharesChart = getResultsSharesChart();\r\n\r\n var rotation = 30;\r\n if (numIndexers > 30) {\r\n rotation = 70;\r\n }\r\n $scope.resultsSharesChart.options.chart.xAxis.rotateLabels = rotation;\r\n $scope.resultsSharesChart.options.chart.height = avgResponseTimesChartHeight;\r\n\r\n $scope.downloadsPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.downloadsPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Downloads');\r\n $scope.downloadsPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.downloadsPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.downloadsPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Downloads');\r\n $scope.downloadsPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.searchesPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.searchesPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Searches');\r\n $scope.searchesPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.searchesPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.searchesPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Searches');\r\n $scope.searchesPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n\r\n $scope.indexerDownloadSharesChart = {\r\n options: {\r\n chart: {\r\n type: 'pieChart',\r\n height: 500,\r\n x: function (d) {\r\n return d.name;\r\n },\r\n y: function (d) {\r\n return d.share;\r\n },\r\n showLabels: true,\r\n duration: 500,\r\n labelThreshold: 0.01,\r\n labelSunbeamLayout: true,\r\n tooltip: {\r\n valueFormatter: function (d, i) {\r\n return $filter('number')(d, 2) + \"%\";\r\n }\r\n },\r\n legend: {\r\n margin: {\r\n top: 5,\r\n right: 35,\r\n bottom: 5,\r\n left: 0\r\n }\r\n }\r\n }\r\n },\r\n data: $scope.indexerDownloadShares\r\n };\r\n\r\n $scope.indexerDownloadSharesChart.options.chart.height = Math.min(Math.max(numIndexers * 40, 350), 900);\r\n };\r\n\r\n\r\n function getChart(chartType, values, xKey, yKey, xAxisLabel, yAxisLabel) {\r\n return {\r\n options: {\r\n chart: {\r\n type: chartType,\r\n height: 350,\r\n margin: {\r\n top: 20,\r\n right: 20,\r\n bottom: 100,\r\n left: 50\r\n },\r\n x: function (d) {\r\n return d[xKey];\r\n },\r\n y: function (d) {\r\n return d[yKey];\r\n },\r\n showValues: true,\r\n valueFormat: function (d) {\r\n return d;\r\n },\r\n color: function () {\r\n return \"red\"\r\n },\r\n showControls: false,\r\n showLegend: false,\r\n duration: 100,\r\n xAxis: {\r\n axisLabel: xAxisLabel,\r\n tickFormat: function (d) {\r\n return d;\r\n },\r\n rotateLabels: 30,\r\n showMaxMin: false,\r\n color: function () {\r\n return \"white\"\r\n }\r\n },\r\n yAxis: {\r\n axisLabel: yAxisLabel,\r\n axisLabelDistance: -10,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n },\r\n tooltip: {\r\n enabled: false\r\n },\r\n zoom: {\r\n enabled: true,\r\n scaleExtent: [1, 10],\r\n useFixedDomain: false,\r\n useNiceScale: false,\r\n horizontalOff: false,\r\n verticalOff: true,\r\n unzoomEventType: 'dblclick.zoom'\r\n }\r\n }\r\n }, data: [{\r\n \"key\": \"doesntmatter\",\r\n \"bar\": true,\r\n \"values\": values\r\n }]\r\n };\r\n }\r\n\r\n //Was unable to use the function above for this and gave up\r\n function getResultsSharesChart() {\r\n return {\r\n options: {\r\n chart: {\r\n type: 'multiBarChart',\r\n height: 350,\r\n margin: {\r\n top: 20,\r\n right: 20,\r\n bottom: 100,\r\n left: 45\r\n },\r\n\r\n clipEdge: true,\r\n duration: 500,\r\n stacked: false,\r\n reduceXTicks: false,\r\n showValues: true,\r\n tooltip: {\r\n enabled: true,\r\n valueFormatter: function (d) {\r\n return d + \"%\";\r\n }\r\n },\r\n showControls: false,\r\n xAxis: {\r\n axisLabel: '',\r\n showMaxMin: false,\r\n rotateLabels: 30,\r\n axisLabelDistance: 30,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n },\r\n yAxis: {\r\n axisLabel: 'Share (%)',\r\n axisLabelDistance: -20,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n }\r\n }\r\n },\r\n\r\n data: [\r\n {\r\n key: \"Results\",\r\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\r\n return {series: 0, y: stats.avgResultsShare, x: stats.name}\r\n })\r\n },\r\n {\r\n key: \"Unique results\",\r\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\r\n return {series: 1, y: stats.avgUniqueResults, x: stats.name}\r\n })\r\n }\r\n ]\r\n };\r\n }\r\n\r\n\r\n}\r\nStatsController.$inject = [\"$scope\", \"$filter\", \"StatsService\", \"blockUI\"];\r\n","//\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('SearchService', SearchService);\r\n\r\nfunction SearchService($http) {\r\n\r\n\r\n var lastExecutedQuery;\r\n var lastResults;\r\n\r\n return {\r\n search: search,\r\n getLastResults: getLastResults,\r\n loadMore: loadMore\r\n };\r\n \r\n\r\n function search(category, query, tmdbid, imdbid, title, tvdbid, rid, season, episode, minsize, maxsize, minage, maxage, indexers, mode) {\r\n var uri;\r\n if (category.indexOf(\"Movies\") > -1 || (category.indexOf(\"20\") == 0) || mode == \"movie\") {\r\n uri = new URI(\"internalapi/moviesearch\");\r\n if (angular.isDefined(tmdbid)) {\r\n uri.addQuery(\"tmdbid\", tmdbid);\r\n } else if (angular.isDefined(imdbid)) {\r\n uri.addQuery(\"imdbid\", imdbid);\r\n } else {\r\n uri.addQuery(\"query\", query);\r\n }\r\n\r\n } else if (category.indexOf(\"TV\") > -1 || (category.indexOf(\"50\") == 0) || mode == \"tvsearch\") {\r\n uri = new URI(\"internalapi/tvsearch\");\r\n if (angular.isDefined(tvdbid)) {\r\n uri.addQuery(\"tvdbid\", tvdbid);\r\n }\r\n if (angular.isDefined(rid)) {\r\n uri.addQuery(\"rid\", rid);\r\n } else {\r\n uri.addQuery(\"query\", query);\r\n }\r\n\r\n if (angular.isDefined(season)) {\r\n uri.addQuery(\"season\", season);\r\n }\r\n if (angular.isDefined(episode)) {\r\n uri.addQuery(\"episode\", episode);\r\n }\r\n } else {\r\n uri = new URI(\"internalapi/search\");\r\n uri.addQuery(\"query\", query);\r\n }\r\n if (angular.isDefined(title)) {\r\n uri.addQuery(\"title\", title);\r\n }\r\n if (_.isNumber(minsize)) {\r\n uri.addQuery(\"minsize\", minsize);\r\n }\r\n if (_.isNumber(maxsize)) {\r\n uri.addQuery(\"maxsize\", maxsize);\r\n }\r\n if (_.isNumber(minage)) {\r\n uri.addQuery(\"minage\", minage);\r\n }\r\n if (_.isNumber(maxage)) {\r\n uri.addQuery(\"maxage\", maxage);\r\n }\r\n if (!angular.isUndefined(indexers)) {\r\n uri.addQuery(\"indexers\", decodeURIComponent(indexers));\r\n }\r\n \r\n\r\n uri.addQuery(\"category\", category);\r\n lastExecutedQuery = uri;\r\n return $http.get(uri.toString()).then(processData);\r\n\r\n }\r\n\r\n function loadMore(offset, loadAll) {\r\n lastExecutedQuery.removeQuery(\"offset\");\r\n lastExecutedQuery.addQuery(\"offset\", offset);\r\n lastExecutedQuery.addQuery(\"loadAll\", loadAll ? true : false);\r\n\r\n return $http.get(lastExecutedQuery.toString()).then(processData);\r\n }\r\n\r\n function processData(response) {\r\n var results = response.data.results;\r\n var indexersearches = response.data.indexersearches;\r\n var total = response.data.total;\r\n var rejected = response.data.rejected;\r\n var resultsCount = results.length;\r\n\r\n\r\n //Sum up response times of indexers from individual api accesses\r\n //TODO: Move this to search result controller because we need to update it every time we loaded more results\r\n _.each(indexersearches, function (ps) {\r\n if (ps.did_search) {\r\n ps.averageResponseTime = _.reduce(ps.apiAccesses, function (memo, rp) {\r\n return memo + rp.response_time;\r\n }, 0);\r\n ps.averageResponseTime = ps.averageResponseTime / ps.apiAccesses.length;\r\n }\r\n });\r\n \r\n lastResults = {\"results\": results, \"indexersearches\": indexersearches, \"total\": total, \"resultsCount\": resultsCount, \"rejected\": rejected};\r\n return lastResults;\r\n }\r\n \r\n function getLastResults() {\r\n return lastResults;\r\n }\r\n}\r\nSearchService.$inject = [\"$http\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('SearchResultsController', SearchResultsController);\r\n\r\nfunction sumRejected(rejected) {\r\n return _.reduce(rejected, function (memo, entry) {\r\n return memo + entry[1];\r\n }, 0);\r\n}\r\n\r\n//SearchResultsController.$inject = ['blockUi'];\r\nfunction SearchResultsController($stateParams, $scope, $q, $timeout, blockUI, growl, localStorageService, SearchService, ConfigService) {\r\n\r\n if (localStorageService.get(\"sorting\") != null) {\r\n var sorting = localStorageService.get(\"sorting\");\r\n $scope.sortPredicate = sorting.predicate;\r\n $scope.sortReversed = sorting.reversed;\r\n } else {\r\n $scope.sortPredicate = \"epoch\";\r\n $scope.sortReversed = true;\r\n }\r\n $scope.limitTo = 100;\r\n $scope.offset = 0;\r\n //Handle incoming data\r\n\r\n $scope.indexersearches = _.sortBy(SearchService.getLastResults().indexersearches, function (i) {\r\n return i.indexer.toLowerCase()\r\n });\r\n $scope.indexerDisplayState = []; //Stores if a indexer's results should be displayed or not\r\n $scope.indexerResultsInfo = {}; //Stores information about the indexer's results like how many we already retrieved\r\n $scope.groupExpanded = {};\r\n $scope.selected = [];\r\n $scope.lastClicked = null;\r\n $scope.lastClickedValue = null;\r\n\r\n $scope.foo = {\r\n indexerStatusesExpanded: localStorageService.get(\"indexerStatusesExpanded\") != null ? localStorageService.get(\"indexerStatusesExpanded\") : false,\r\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") != null ? localStorageService.get(\"duplicatesDisplayed\") : false\r\n };\r\n\r\n $scope.countFilteredOut = 0;\r\n\r\n //Initially set visibility of all found indexers to true, they're needed for initial filtering / sorting\r\n _.forEach($scope.indexersearches, function (ps) {\r\n $scope.indexerDisplayState[ps.indexer.toLowerCase()] = true;\r\n });\r\n\r\n _.forEach($scope.indexersearches, function (ps) {\r\n $scope.indexerResultsInfo[ps.indexer.toLowerCase()] = {loadedResults: ps.loaded_results};\r\n });\r\n\r\n //Process results\r\n $scope.results = SearchService.getLastResults().results;\r\n $scope.total = SearchService.getLastResults().total;\r\n $scope.resultsCount = SearchService.getLastResults().resultsCount;\r\n $scope.rejected = SearchService.getLastResults().rejected;\r\n $scope.countRejected = sumRejected($scope.rejected);\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n\r\n $scope.$emit(\"searchResultsShown\");\r\n stopBlocking();\r\n\r\n //Returns the content of the property (defined by the current sortPredicate) of the first group element \r\n $scope.firstResultPredicate = firstResultPredicate;\r\n function firstResultPredicate(item) {\r\n return item[0][$scope.sortPredicate];\r\n }\r\n\r\n //Returns the unique group identifier which allows angular to keep track of the grouped search results even after filtering, making filtering by indexers a lot faster (albeit still somewhat slow...) \r\n $scope.groupId = groupId;\r\n function groupId(item) {\r\n return item[0][0].searchResultId;\r\n }\r\n\r\n //Block the UI and return after timeout. This way we make sure that the blocking is done before angular starts updating the model/view. There's probably a better way to achieve that?\r\n function startBlocking(message) {\r\n var deferred = $q.defer();\r\n blockUI.start(message);\r\n $timeout(function () {\r\n deferred.resolve();\r\n }, 100);\r\n return deferred.promise;\r\n }\r\n\r\n //Set sorting according to the predicate. If it's the same as the old one, reverse, if not sort by the given default (so that age is descending, name ascending, etc.)\r\n //Sorting (and filtering) are really slow (about 2 seconds for 1000 results from 5 indexers) but I haven't found any way of making it faster, apart from the tracking \r\n $scope.setSorting = setSorting;\r\n function setSorting(predicate, reversedDefault) {\r\n if (predicate == $scope.sortPredicate) {\r\n $scope.sortReversed = !$scope.sortReversed;\r\n } else {\r\n $scope.sortReversed = reversedDefault;\r\n }\r\n $scope.sortPredicate = predicate;\r\n startBlocking(\"Sorting / filtering...\").then(function () {\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n blockUI.reset();\r\n localStorageService.set(\"sorting\", {predicate: predicate, reversed: $scope.sortReversed});\r\n });\r\n }\r\n\r\n $scope.inlineFilter = inlineFilter;\r\n function inlineFilter(result) {\r\n var ok = true;\r\n ok = ok && $scope.titleFilter && result.title.toLowerCase().indexOf($scope.titleFilter) > -1;\r\n ok = ok && $scope.minSizeFilter && $scope.minSizeFilter * 1024 * 1024 < result.size;\r\n ok = ok && $scope.maxSizeFilter && $scope.maxSizeFilter * 1024 * 1024 > result.size;\r\n return ok;\r\n }\r\n\r\n\r\n $scope.$on(\"searchInputChanged\", function (event, query, minage, maxage, minsize, maxsize) {\r\n $scope.filteredResults = sortAndFilter($scope.results, query, minage, maxage, minsize, maxsize);\r\n });\r\n\r\n $scope.resort = function () {\r\n };\r\n\r\n function sortAndFilter(results, query, minage, maxage, minsize, maxsize) {\r\n $scope.countFilteredOut = 0;\r\n\r\n function filterByAgeAndSize(item) {\r\n var ok = true;\r\n ok = ok && (!_.isNumber(minsize) || item.size / 1024 / 1024 >= minsize)\r\n && (!_.isNumber(maxsize) || item.size / 1024 / 1024 <= maxsize)\r\n && (!_.isNumber(minage) || item.age_days >= Number(minage))\r\n && (!_.isNumber(maxage) || item.age_days <= Number(maxage));\r\n\r\n if (ok && query) {\r\n var words = query.toLowerCase().split(\" \");\r\n ok = _.every(words, function (word) {\r\n return item.title.toLowerCase().indexOf(word) > -1;\r\n });\r\n }\r\n if (!ok) {\r\n $scope.countFilteredOut++;\r\n }\r\n return ok;\r\n }\r\n\r\n\r\n function getItemIndexerDisplayState(item) {\r\n return $scope.indexerDisplayState[item.indexer.toLowerCase()];\r\n }\r\n\r\n function getCleanedTitle(element) {\r\n return element.title.toLowerCase().replace(/[\\s\\-\\._]/ig, \"\");\r\n }\r\n\r\n function createSortedHashgroups(titleGroup) {\r\n\r\n function createHashGroup(hashGroup) {\r\n //Sorting hash group's contents should not matter for size and age and title but might for category (we might remove this, it's probably mostly unnecessary)\r\n var sortedHashGroup = _.sortBy(hashGroup, function (item) {\r\n var sortPredicateValue;\r\n if ($scope.sortPredicate == \"grabs\") {\r\n sortPredicateValue = angular.isDefined(item.grabs) ? item.grabs : 0;\r\n } else {\r\n sortPredicateValue = item[$scope.sortPredicate];\r\n }\r\n //var sortPredicateValue = item[$scope.sortPredicate];\r\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\r\n });\r\n //Now sort the hash group by indexer score (inverted) so that the result with the highest indexer score is shown on top (or as the only one of a hash group if it's collapsed)\r\n sortedHashGroup = _.sortBy(sortedHashGroup, function (item) {\r\n return item.indexerscore * -1;\r\n });\r\n return sortedHashGroup;\r\n }\r\n\r\n function getHashGroupFirstElementSortPredicate(hashGroup) {\r\n if ($scope.sortPredicate == \"grabs\") {\r\n sortPredicateValue = angular.isDefined(hashGroup[0].grabs) ? hashGroup[0].grabs : 0;\r\n } else {\r\n var sortPredicateValue = hashGroup[0][$scope.sortPredicate];\r\n }\r\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\r\n }\r\n\r\n return _.chain(titleGroup).groupBy(\"hash\").map(createHashGroup).sortBy(getHashGroupFirstElementSortPredicate).value();\r\n }\r\n\r\n function getTitleGroupFirstElementsSortPredicate(titleGroup) {\r\n var sortPredicateValue;\r\n if ($scope.sortPredicate == \"title\") {\r\n sortPredicateValue = titleGroup[0][0].title.toLowerCase();\r\n } else if ($scope.sortPredicate == \"grabs\") {\r\n sortPredicateValue = angular.isDefined(titleGroup[0][0].grabs) ? titleGroup[0][0].grabs : 0;\r\n } else {\r\n sortPredicateValue = titleGroup[0][0][$scope.sortPredicate];\r\n }\r\n\r\n return sortPredicateValue;\r\n }\r\n\r\n var filtered = _.chain(results)\r\n //Filter by age, size and title\r\n .filter(filterByAgeAndSize)\r\n //Remove elements of which the indexer is currently hidden \r\n .filter(getItemIndexerDisplayState)\r\n //Make groups of results with the same title \r\n .groupBy(getCleanedTitle)\r\n //For every title group make subgroups of duplicates and sort the group \r\n .map(createSortedHashgroups)\r\n //And then sort the title group using its first hashgroup's first item (the group itself is already sorted and so are the hash groups) \r\n .sortBy(getTitleGroupFirstElementsSortPredicate)\r\n .value();\r\n if ($scope.sortReversed) {\r\n filtered = filtered.reverse();\r\n }\r\n if ($scope.countFilteredOut > 0) {\r\n growl.info(\"Filtered \" + $scope.countFilteredOut + \" of the retrieved results\");\r\n }\r\n\r\n $scope.lastClicked = null;\r\n return filtered;\r\n }\r\n\r\n $scope.toggleTitlegroupExpand = function toggleTitlegroupExpand(titleGroup) {\r\n $scope.groupExpanded[titleGroup[0][0].title] = !$scope.groupExpanded[titleGroup[0][0].title];\r\n $scope.groupExpanded[titleGroup[0][0].hash] = !$scope.groupExpanded[titleGroup[0][0].hash];\r\n };\r\n\r\n\r\n $scope.stopBlocking = stopBlocking;\r\n function stopBlocking() {\r\n blockUI.reset();\r\n }\r\n\r\n $scope.loadMore = loadMore;\r\n function loadMore(loadAll) {\r\n startBlocking(loadAll ? \"Loading all results...\" : \"Loading more results...\").then(function () {\r\n SearchService.loadMore($scope.resultsCount, loadAll).then(function (data) {\r\n $scope.results = $scope.results.concat(data.results);\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n $scope.total = data.total;\r\n $scope.rejected = data.rejected;\r\n $scope.countRejected = sumRejected($scope.rejected);\r\n $scope.resultsCount += data.resultsCount;\r\n stopBlocking();\r\n });\r\n });\r\n }\r\n\r\n\r\n//Filters the results according to new visibility settings.\r\n $scope.toggleIndexerDisplay = toggleIndexerDisplay;\r\n function toggleIndexerDisplay(indexer) {\r\n $scope.indexerDisplayState[indexer.toLowerCase()] = $scope.indexerDisplayState[indexer.toLowerCase()];\r\n startBlocking(\"Filtering. Sorry...\").then(function () {\r\n $scope.filteredResults = sortAndFilter($scope.results);\r\n }).then(function () {\r\n stopBlocking();\r\n });\r\n }\r\n\r\n $scope.countResults = countResults;\r\n function countResults() {\r\n return $scope.results.length;\r\n }\r\n\r\n $scope.invertSelection = function invertSelection() {\r\n $scope.$broadcast(\"invertSelection\");\r\n };\r\n\r\n $scope.toggleIndexerStatuses = function () {\r\n $scope.foo.indexerStatusesExpanded = !$scope.foo.indexerStatusesExpanded;\r\n localStorageService.set(\"indexerStatusesExpanded\", $scope.foo.indexerStatusesExpanded);\r\n };\r\n\r\n $scope.toggleDuplicatesDisplayed = function () {\r\n //$scope.foo.duplicatesDisplayed = !$scope.foo.duplicatesDisplayed;\r\n localStorageService.set(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\r\n $scope.$broadcast(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\r\n };\r\n\r\n $scope.$on(\"checkboxClicked\", function (event, originalEvent, rowIndex, newCheckedValue) {\r\n if (originalEvent.shiftKey && $scope.lastClicked != null) {\r\n $scope.$broadcast(\"shiftClick\", Number($scope.lastClicked), Number(rowIndex), Number($scope.lastClickedValue));\r\n }\r\n $scope.lastClicked = rowIndex;\r\n $scope.lastClickedValue = newCheckedValue;\r\n })\r\n\r\n $scope.filterRejectedZero = function() {\r\n return function (entry) {\r\n return entry[1] > 0;\r\n }\r\n }\r\n}\r\nSearchResultsController.$inject = [\"$stateParams\", \"$scope\", \"$q\", \"$timeout\", \"blockUI\", \"growl\", \"localStorageService\", \"SearchService\", \"ConfigService\"];\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('SearchHistoryService', SearchHistoryService);\r\n\r\nfunction SearchHistoryService($filter, $http) {\r\n\r\n return {\r\n getSearchHistory: getSearchHistory,\r\n getSearchHistoryForSearching: getSearchHistoryForSearching,\r\n formatRequest: formatRequest,\r\n getStateParamsForRepeatedSearch: getStateParamsForRepeatedSearch\r\n };\r\n\r\n function getSearchHistoryForSearching() {\r\n return $http.post(\"internalapi/getsearchrequestsforsearching\").success(function (response) {\r\n return {\r\n searchRequests: response.searchRequests,\r\n totalRequests: response.totalRequests\r\n }\r\n });\r\n }\r\n\r\n function getSearchHistory(pageNumber, limit, filterModel, sortModel, distinct, onlyCurrentUser) {\r\n var params = {\r\n page: pageNumber,\r\n limit: limit,\r\n filterModel: filterModel,\r\n distinct: distinct,\r\n onlyCurrentUser: onlyCurrentUser\r\n };\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n }\r\n return $http.post(\"internalapi/getsearchrequests\", params).success(function (response) {\r\n return {\r\n searchRequests: response.searchRequests,\r\n totalRequests: response.totalRequests\r\n }\r\n });\r\n }\r\n\r\n function formatRequest(request, includeIdLink, includequery, describeEmptySearch, includeTitle) {\r\n var result = [];\r\n //ID key: ID value\r\n //season\r\n //episode\r\n //author\r\n //title\r\n if (includequery && request.query) {\r\n result.push(\"Query: \" + request.query);\r\n }\r\n if (request.title && includeTitle) {\r\n result.push('Title: ' + request.title);\r\n } else if (request.movietitle && includeTitle) {\r\n result.push('Title: ' + request.movietitle);\r\n } else if (request.tvtitle && includeTitle) {\r\n result.push('Title: ' + request.tvtitle);\r\n } else if (request.identifier_key) {\r\n var href;\r\n var key;\r\n if (request.identifier_key == \"imdbid\") {\r\n key = \"IMDB ID\";\r\n href = \"https://www.imdb.com/title/tt\"\r\n } else if (request.identifier_key == \"tvdbid\") {\r\n key = \"TVDB ID\";\r\n href = \"https://thetvdb.com/?tab=series&id=\"\r\n } else if (request.identifier_key == \"rid\") {\r\n key = \"TVRage ID\";\r\n href = \"internalapi/redirect_rid?rid=\"\r\n } else if (request.identifier_key == \"tmdb\") {\r\n key = \"TMDV ID\";\r\n href = \"https://www.themoviedb.org/movie/\"\r\n }\r\n href = href + request.identifier_value;\r\n href = $filter(\"dereferer\")(href);\r\n if (includeIdLink) {\r\n result.push('' + key + ': ' + request.identifier_value + \"\");\r\n } else {\r\n result.push('' + key + \": \" + request.identifier_value);\r\n }\r\n }\r\n if (request.season) {\r\n result.push('Season: ' + request.season);\r\n }\r\n if (request.episode) {\r\n result.push('Episode: ' + request.episode);\r\n }\r\n if (request.author) {\r\n result.push('Author: ' + request.author);\r\n }\r\n if (result.length == 0 && describeEmptySearch) {\r\n result = ['Empty search'];\r\n }\r\n\r\n return result.join(\", \");\r\n\r\n }\r\n\r\n function getStateParamsForRepeatedSearch(request) {\r\n var stateParams = {};\r\n stateParams.mode = \"search\"\r\n if (request.identifier_key == \"imdbid\") {\r\n stateParams.mode = \"movie\"\r\n stateParams.imdbid = request.identifier_value;\r\n } else if (request.identifier_key == \"tvdbid\" || request.identifier_key == \"rid\") {\r\n stateParams.mode = \"tvsearch\";\r\n if (request.identifier_key == \"rid\") {\r\n stateParams.rid = request.identifier_value;\r\n } else {\r\n stateParams.tvdbid = request.identifier_value;\r\n }\r\n\r\n if (request.season != \"\") {\r\n stateParams.season = request.season;\r\n }\r\n if (request.episode != \"\") {\r\n stateParams.episode = request.episode;\r\n }\r\n }\r\n if (request.query != \"\") {\r\n stateParams.query = request.query;\r\n }\r\n\r\n\r\n if (request.movietitle != null) {\r\n stateParams.title = request.movietitle;\r\n }\r\n if (request.tvtitle != null) {\r\n stateParams.title = request.tvtitle;\r\n }\r\n\r\n if (request.category) {\r\n stateParams.category = request.category;\r\n }\r\n\r\n stateParams.category = request.category;\r\n\r\n return stateParams;\r\n }\r\n\r\n\r\n}\r\nSearchHistoryService.$inject = [\"$filter\", \"$http\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('SearchHistoryController', SearchHistoryController);\r\n\r\n\r\nfunction SearchHistoryController($scope, $state, SearchHistoryService, ConfigService, history, $sce, $filter) {\r\n $scope.limit = 100;\r\n $scope.pagination = {\r\n current: 1\r\n };\r\n $scope.sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n $scope.filterModel = {};\r\n\r\n //Filter options\r\n $scope.categoriesForFiltering = [];\r\n _.forEach(ConfigService.getSafe().categories, function (category) {\r\n $scope.categoriesForFiltering.push({label: category.pretty, id: category.pretty})\r\n });\r\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\r\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: false}, {label: \"Internal\", value: true}];\r\n\r\n //Preloaded data\r\n $scope.searchRequests = history.data.searchRequests;\r\n $scope.totalRequests = history.data.totalRequests;\r\n\r\n $scope.update = function () {\r\n SearchHistoryService.getSearchHistory($scope.pagination.current, $scope.limit, $scope.filterModel, $scope.sortModel).then(function (history) {\r\n $scope.searchRequests = history.data.searchRequests;\r\n $scope.totalRequests = history.data.totalRequests;\r\n });\r\n };\r\n\r\n $scope.$on(\"sort\", function (event, column, sortMode) {\r\n if (sortMode == 0) {\r\n column = \"time\";\r\n sortMode = 2;\r\n }\r\n $scope.sortModel = {\r\n column: column,\r\n sortMode: sortMode\r\n };\r\n $scope.$broadcast(\"newSortColumn\", column);\r\n $scope.update();\r\n });\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\r\n if (filterModel.filter) {\r\n $scope.filterModel[column] = filterModel;\r\n } else {\r\n delete $scope.filterModel[column];\r\n }\r\n $scope.update();\r\n });\r\n\r\n\r\n $scope.openSearch = function (request) {\r\n var stateParams = {};\r\n if (request.identifier_key == \"imdbid\") {\r\n stateParams.imdbid = request.identifier_value;\r\n } else if (request.identifier_key == \"tvdbid\" || request.identifier_key == \"rid\") {\r\n if (request.identifier_key == \"rid\") {\r\n stateParams.rid = request.identifier_value;\r\n } else {\r\n stateParams.tvdbid = request.identifier_value;\r\n }\r\n\r\n if (request.season != \"\") {\r\n stateParams.season = request.season;\r\n }\r\n if (request.episode != \"\") {\r\n stateParams.episode = request.episode;\r\n }\r\n }\r\n if (request.query != \"\") {\r\n stateParams.query = request.query;\r\n }\r\n if (request.type == \"tv\") {\r\n stateParams.mode = \"tvsearch\"\r\n } else if (request.type == \"movie\") {\r\n stateParams.mode = \"movie\"\r\n } else {\r\n stateParams.mode = \"search\"\r\n }\r\n\r\n if (request.movietitle != null) {\r\n stateParams.title = request.movietitle;\r\n }\r\n if (request.tvtitle != null) {\r\n stateParams.title = request.tvtitle;\r\n }\r\n\r\n if (request.category) {\r\n stateParams.category = request.category;\r\n }\r\n\r\n stateParams.category = request.category;\r\n\r\n $state.go(\"root.search\", stateParams, {inherit: false});\r\n };\r\n\r\n $scope.formatQuery = function (request) {\r\n if (request.movietitle != null) {\r\n return request.movietitle;\r\n }\r\n if (request.tvtitle != null) {\r\n return request.tvtitle;\r\n }\r\n\r\n if (!request.query && !request.identifier_key && !request.season && !request.episode) {\r\n return \"Update query\";\r\n }\r\n return request.query;\r\n };\r\n\r\n $scope.formatAdditional = function (request) {\r\n var result = [];\r\n //ID key: ID value\r\n //season\r\n //episode\r\n //author\r\n //title\r\n if (request.identifier_key) {\r\n var href;\r\n var key;\r\n if (request.identifier_key == \"imdbid\") {\r\n key = \"IMDB ID\";\r\n href = \"https://www.imdb.com/title/tt\"\r\n } else if (request.identifier_key == \"tvdbid\") {\r\n key = \"TVDB ID\";\r\n href = \"https://thetvdb.com/?tab=series&id=\"\r\n } else if (request.identifier_key == \"rid\") {\r\n key = \"TVRage ID\";\r\n href = \"internalapi/redirect_rid?rid=\"\r\n } else if (request.identifier_key == \"tmdb\") {\r\n key = \"TMDV ID\";\r\n href = \"https://www.themoviedb.org/movie/\"\r\n }\r\n href = href + request.identifier_value;\r\n href = $filter(\"dereferer\")(href);\r\n result.push(key + \": \" + '' + request.identifier_value + \"\");\r\n }\r\n if (request.season) {\r\n result.push(\"Season: \" + request.season);\r\n }\r\n if (request.episode) {\r\n result.push(\"Episode: \" + request.episode);\r\n }\r\n if (request.author) {\r\n result.push(\"Author: \" + request.author);\r\n }\r\n if (request.title) {\r\n result.push(\"Title: \" + request.title);\r\n }\r\n return $sce.trustAsHtml(result.join(\", \"));\r\n };\r\n\r\n\r\n\r\n\r\n}\r\nSearchHistoryController.$inject = [\"$scope\", \"$state\", \"SearchHistoryService\", \"ConfigService\", \"history\", \"$sce\", \"$filter\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('SearchController', SearchController);\r\n\r\nfunction SearchController($scope, $http, $stateParams, $state, $window, $filter, $sce, growl, SearchService, focus, ConfigService, CategoriesService, blockUI, $element, ModalService, SearchHistoryService) {\r\n\r\n function getNumberOrUndefined(number) {\r\n if (_.isUndefined(number) || _.isNaN(number) || number == \"\") {\r\n return undefined;\r\n }\r\n number = parseInt(number);\r\n if (_.isNumber(number)) {\r\n return number;\r\n } else {\r\n return undefined;\r\n }\r\n }\r\n\r\n //Fill the form with the search values we got from the state params (so that their values are the same as in the current url)\r\n $scope.mode = $stateParams.mode;\r\n $scope.categories = _.filter(CategoriesService.getAll(), function (c) {\r\n return c.mayBeSelected && c.ignoreResults != \"internal\" && c.ignoreResults != \"always\";\r\n });\r\n if (angular.isDefined($stateParams.category) && $stateParams.category) {\r\n $scope.category = CategoriesService.getByName($stateParams.category);\r\n } else {\r\n $scope.category = CategoriesService.getDefault();\r\n }\r\n $scope.category = (_.isUndefined($stateParams.category) || $stateParams.category == \"\") ? CategoriesService.getDefault() : CategoriesService.getByName($stateParams.category);\r\n $scope.tmdbid = $stateParams.tmdbid;\r\n $scope.tvdbid = $stateParams.tvdbid;\r\n $scope.imdbid = $stateParams.imdbid;\r\n $scope.rid = $stateParams.rid;\r\n $scope.title = $stateParams.title;\r\n $scope.season = $stateParams.season;\r\n $scope.episode = $stateParams.episode;\r\n $scope.query = $stateParams.query;\r\n $scope.minsize = getNumberOrUndefined($stateParams.minsize);\r\n $scope.maxsize = getNumberOrUndefined($stateParams.maxsize);\r\n $scope.minage = getNumberOrUndefined($stateParams.minage);\r\n $scope.maxage = getNumberOrUndefined($stateParams.maxage);\r\n if (!_.isUndefined($scope.title) && _.isUndefined($scope.query)) {\r\n //$scope.query = $scope.title;\r\n }\r\n if (!angular.isUndefined($stateParams.indexers)) {\r\n $scope.indexers = decodeURIComponent($stateParams.indexers).split(\"|\");\r\n }\r\n\r\n $scope.showIndexers = {};\r\n\r\n $scope.searchHistory = [];\r\n\r\n var safeConfig = ConfigService.getSafe();\r\n\r\n //Doesn't belong here but whatever\r\n var firstStartThreeDaysAgo = ConfigService.getSafe().firstStart < moment().subtract(3, \"days\").unix();\r\n var doShowSurvey = (ConfigService.getSafe().pollShown == 0 && firstStartThreeDaysAgo) || ConfigService.getSafe().pollShown == 1;\r\n if (doShowSurvey) {\r\n var message;\r\n if (ConfigService.getSafe().pollShown == 0) {\r\n message = \"Dear user, I would like to ask you to answer a short query about NZB Hydra. It is absolutely anonymous and will not take more than a couple of minutes. You would help me a lot!\";\r\n } else {\r\n message = \"Dear user, thank you for answering my last survey. Unfortunately I'm an idiot and didn't know that SurveyMonkey would only show me the first 100 results. Please be so kind and answer the new survey :-)\";\r\n }\r\n ModalService.open(\"User query\",\r\n message, {\r\n yes: {\r\n onYes: function () {\r\n $window.open($filter(\"dereferer\")(\"https://goo.gl/forms/F3PwtEor2krBxLcR2\"), \"_blank\");\r\n $http.get(\"internalapi/pollshown\", {params: {selection: 1}});\r\n ConfigService.getSafe().pollShown = 2;\r\n },\r\n text: \"Yes, I want to help. Take me there.\"\r\n },\r\n cancel: {\r\n onCancel: function () {\r\n $http.get(\"internalapi/pollshown\", {params: {selection: 0}});\r\n ConfigService.getSafe().pollShown = 0;\r\n },\r\n text: \"Not now. Remind me.\"\r\n },\r\n no: {\r\n onNo: function () {\r\n $http.get(\"internalapi/pollshown\", {params: {selection: -1}});\r\n ConfigService.getSafe().pollShown = -1;\r\n },\r\n text: \"Nah, feck off!\"\r\n }\r\n });\r\n }\r\n\r\n\r\n $scope.typeAheadWait = 300;\r\n $scope.selectedItem = \"\";\r\n $scope.autocompleteLoading = false;\r\n $scope.isAskById = $scope.category.supportsById;\r\n $scope.isById = {value: true}; //If true the user wants to search by id so we enable autosearch. Was unable to achieve this using a simple boolean\r\n $scope.availableIndexers = [];\r\n $scope.autocompleteClass = \"autocompletePosterMovies\";\r\n\r\n $scope.toggle = function (searchCategory) {\r\n $scope.category = searchCategory;\r\n\r\n //Show checkbox to ask if the user wants to search by ID (using autocomplete)\r\n $scope.isAskById = $scope.category.supportsById;\r\n\r\n focus('searchfield');\r\n\r\n //Hacky way of triggering the autocomplete loading\r\n var searchModel = $element.find(\"#searchfield\").controller(\"ngModel\");\r\n if (angular.isDefined(searchModel.$viewValue)) {\r\n searchModel.$setViewValue(searchModel.$viewValue + \" \");\r\n }\r\n\r\n if (safeConfig.searching.enableCategorySizes) {\r\n var min = searchCategory.min;\r\n var max = searchCategory.max;\r\n if (_.isNumber(min)) {\r\n $scope.minsize = min;\r\n } else {\r\n $scope.minsize = \"\";\r\n }\r\n if (_.isNumber(max)) {\r\n $scope.maxsize = max;\r\n } else {\r\n $scope.maxsize = \"\";\r\n }\r\n }\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n\r\n\r\n };\r\n\r\n\r\n // Any function returning a promise object can be used to load values asynchronously\r\n $scope.getAutocomplete = function (val) {\r\n $scope.autocompleteLoading = true;\r\n //Expected model returned from API:\r\n //label: What to show in the results\r\n //title: Will be used for file search\r\n //value: Will be used as extraInfo (ttid oder tvdb id)\r\n //poster: url of poster to show\r\n\r\n //Don't use autocomplete if checkbox is disabled\r\n if (!$scope.isById.value) {\r\n return {};\r\n }\r\n\r\n if ($scope.category.name.indexOf(\"movies\") > -1) {\r\n return $http.get('internalapi/autocomplete?type=movie', {\r\n params: {\r\n input: val\r\n }\r\n }).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data.results;\r\n });\r\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\r\n\r\n return $http.get('internalapi/autocomplete?type=tv', {\r\n params: {\r\n input: val\r\n }\r\n }).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data.results;\r\n });\r\n } else {\r\n return {};\r\n }\r\n };\r\n\r\n\r\n $scope.startSearch = function () {\r\n blockUI.start(\"Searching...\");\r\n var indexers = angular.isUndefined($scope.indexers) ? undefined : $scope.indexers.join(\"|\");\r\n SearchService.search($scope.category.name, $scope.query, $scope.tmdbid, $scope.imdbid, $scope.title, $scope.tvdbid, $scope.rid, $scope.season, $scope.episode, $scope.minsize, $scope.maxsize, $scope.minage, $scope.maxage, indexers, $scope.mode).then(function () {\r\n $state.go(\"root.search.results\", {\r\n minsize: $scope.minsize,\r\n maxsize: $scope.maxsize,\r\n minage: $scope.minage,\r\n maxage: $scope.maxage\r\n }, {\r\n inherit: true\r\n });\r\n $scope.tmdbid = undefined;\r\n $scope.imdbid = undefined;\r\n $scope.tvdbid = undefined;\r\n });\r\n };\r\n\r\n function getSelectedIndexers() {\r\n var activatedIndexers = _.filter($scope.availableIndexers).filter(function (indexer) {\r\n return indexer.activated;\r\n });\r\n return _.pluck(activatedIndexers, \"name\").join(\"|\");\r\n }\r\n\r\n\r\n $scope.goToSearchUrl = function () {\r\n var stateParams = {};\r\n if ($scope.category.name.indexOf(\"movies\") > -1) {\r\n stateParams.title = $scope.title;\r\n stateParams.mode = \"movie\";\r\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\r\n stateParams.mode = \"tvsearch\";\r\n stateParams.title = $scope.title;\r\n } else if ($scope.category.name == \"ebook\") {\r\n stateParams.mode = \"ebook\";\r\n } else {\r\n stateParams.mode = \"search\";\r\n }\r\n\r\n stateParams.tmdbid = $scope.tmdbid;\r\n stateParams.tvdbid = $scope.tvdbid;\r\n stateParams.title = $scope.title;\r\n stateParams.season = $scope.season;\r\n stateParams.episode = $scope.episode;\r\n stateParams.query = $scope.query;\r\n stateParams.minsize = $scope.minsize;\r\n stateParams.maxsize = $scope.maxsize;\r\n stateParams.minage = $scope.minage;\r\n stateParams.maxage = $scope.maxage;\r\n stateParams.category = $scope.category.name;\r\n stateParams.indexers = encodeURIComponent(getSelectedIndexers());\r\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.repeatSearch = function (request) {\r\n $state.go(\"root.search\", SearchHistoryService.getStateParamsForRepeatedSearch(request), {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n\r\n $scope.selectAutocompleteItem = function ($item) {\r\n $scope.selectedItem = $item;\r\n $scope.title = $item.title;\r\n if ($scope.category.name.indexOf(\"movies\") > -1) {\r\n $scope.tmdbid = $item.value;\r\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\r\n $scope.tvdbid = $item.value;\r\n }\r\n $scope.query = \"\";\r\n $scope.goToSearchUrl();\r\n };\r\n\r\n $scope.startQuerySearch = function () {\r\n if (!$scope.query) {\r\n growl.error(\"You didn't enter a query...\");\r\n } else {\r\n //Reset values because they might've been set from the last search\r\n $scope.title = undefined;\r\n $scope.tmdbid = undefined;\r\n $scope.tvdbid = undefined;\r\n $scope.season = undefined;\r\n $scope.episode = undefined;\r\n $scope.goToSearchUrl();\r\n }\r\n };\r\n\r\n\r\n $scope.autocompleteActive = function () {\r\n return $scope.category.supportsById;\r\n };\r\n\r\n $scope.seriesSelected = function () {\r\n return $scope.category.name.indexOf(\"tv\") > -1;\r\n };\r\n\r\n $scope.toggleIndexer = function (indexer) {\r\n $scope.indexers[indexer] = !$scope.indexers[indexer]\r\n };\r\n\r\n\r\n function isIndexerPreselected(indexer) {\r\n if (angular.isUndefined($scope.indexers)) {\r\n return indexer.preselect;\r\n } else {\r\n return _.contains($scope.indexers, indexer.name);\r\n }\r\n\r\n }\r\n\r\n\r\n function getAvailableIndexers() {\r\n return _.chain(safeConfig.indexers).filter(function (indexer) {\r\n return indexer.enabled && indexer.showOnSearch && (angular.isUndefined(indexer.categories) || indexer.categories.length == 0 || $scope.category.name == \"all\" || indexer.categories.indexOf($scope.category.name) > -1);\r\n }).sortBy(function (indexer) {\r\n return indexer.name.toLowerCase();\r\n })\r\n .map(function (indexer) {\r\n return {name: indexer.name, activated: isIndexerPreselected(indexer), categories: indexer.categories};\r\n }).value();\r\n }\r\n\r\n\r\n $scope.toggleAllIndexers = function () {\r\n angular.forEach($scope.availableIndexers, function (indexer) {\r\n indexer.activated = !indexer.activated;\r\n })\r\n };\r\n\r\n $scope.searchInputChanged = function () {\r\n $scope.$broadcast(\"searchInputChanged\", $scope.query != $stateParams.query ? $scope.query : null, $scope.minage, $scope.maxage, $scope.minsize, $scope.maxsize);\r\n };\r\n\r\n\r\n $scope.formatRequest = function (request) {\r\n return $sce.trustAsHtml(SearchHistoryService.formatRequest(request, false, true, true, true));\r\n };\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n\r\n\r\n function getAndSetSearchRequests() {\r\n SearchHistoryService.getSearchHistoryForSearching().then(function (data) {\r\n $scope.searchHistory = data.data.searchRequests;\r\n });\r\n }\r\n\r\n if ($scope.mode) {\r\n $scope.startSearch();\r\n } else {\r\n //Getting the search history only makes sense when we're not currently searching\r\n getAndSetSearchRequests();\r\n }\r\n\r\n $scope.$on(\"searchResultsShown\", function() {\r\n getAndSetSearchRequests();\r\n });\r\n\r\n\r\n\r\n\r\n}\r\nSearchController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\", \"$window\", \"$filter\", \"$sce\", \"growl\", \"SearchService\", \"focus\", \"ConfigService\", \"CategoriesService\", \"blockUI\", \"$element\", \"ModalService\", \"SearchHistoryService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('RestartService', RestartService);\r\n\r\nfunction RestartService(blockUI, $timeout, $window, growl, NzbHydraControlService) {\r\n\r\n return {\r\n restart: restart,\r\n countdown: countdown\r\n };\r\n\r\n\r\n function internalCaR(message, timer) {\r\n\r\n if (timer >= 1) {\r\n blockUI.start(message + \"Restarting. Will reload page in \" + timer + \" seconds...\");\r\n $timeout(function () {\r\n internalCaR(message, timer - 1)\r\n }, 1000);\r\n } else {\r\n $timeout(function () {\r\n blockUI.start(\"Reloading page...\");\r\n $window.location.reload();\r\n }, 1000);\r\n }\r\n }\r\n \r\n function countdown() {\r\n internalCaR(\"\", 15);\r\n }\r\n\r\n function restart(message) {\r\n message = angular.isDefined(message) ? message + \" \" : \"\";\r\n NzbHydraControlService.restart().then(internalCaR(message, 15),\r\n function () {\r\n growl.info(\"Unable to send restart command.\");\r\n }\r\n )\r\n }\r\n}\r\nRestartService.$inject = [\"blockUI\", \"$timeout\", \"$window\", \"growl\", \"NzbHydraControlService\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('NzbHydraControlService', NzbHydraControlService);\r\n\r\nfunction NzbHydraControlService($http) {\r\n\r\n return {\r\n restart: restart,\r\n shutdown: shutdown,\r\n deleteLogAndDb: deleteLogAndDb\r\n };\r\n\r\n function restart() {\r\n return $http.get(\"internalapi/restart\");\r\n }\r\n\r\n function shutdown() {\r\n return $http.get(\"internalapi/shutdown\");\r\n }\r\n\r\n function deleteLogAndDb() {\r\n return $http.get(\"internalapi/deleteloganddb\");\r\n }\r\n}\r\nNzbHydraControlService.$inject = [\"$http\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('NzbDownloadService', NzbDownloadService);\r\n\r\nfunction NzbDownloadService($http, ConfigService, DownloaderCategoriesService) {\r\n\r\n var service = {\r\n download: download,\r\n getEnabledDownloaders: getEnabledDownloaders\r\n };\r\n\r\n return service;\r\n\r\n function sendNzbAddCommand(downloader, searchresultids, category) {\r\n var params = {downloader: downloader.name, searchresultids: angular.toJson(searchresultids)};\r\n if (category != \"No category\") {\r\n params[\"category\"] = category;\r\n }\r\n return $http.put(\"internalapi/addnzbs\", params);\r\n }\r\n \r\n function download(downloader, searchresultids) {\r\n \r\n var category = downloader.defaultCategory;\r\n \r\n if ((_.isUndefined(category) || category == \"\" || category == null) && category != \"No category\") {\r\n return DownloaderCategoriesService.openCategorySelection(downloader).then(function (category) {\r\n return sendNzbAddCommand(downloader, searchresultids, category)\r\n }, function (error) {\r\n throw error;\r\n });\r\n } else {\r\n return sendNzbAddCommand(downloader, searchresultids, category)\r\n }\r\n }\r\n \r\n function getEnabledDownloaders() {\r\n return _.filter(ConfigService.getSafe().downloaders, \"enabled\");\r\n }\r\n}\r\nNzbDownloadService.$inject = [\"$http\", \"ConfigService\", \"DownloaderCategoriesService\"];\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('ModalService', ModalService);\r\n\r\nfunction ModalService($uibModal, $q) {\r\n \r\n return {\r\n open: open\r\n };\r\n \r\n function open(headline, message, params, size) {\r\n //params example:\r\n /*\r\n var p =\r\n {\r\n yes: {\r\n text: \"Yes\", //default: Ok\r\n onYes: function() {}\r\n },\r\n no: { //default: Empty\r\n text: \"No\",\r\n onNo: function () {\r\n }\r\n },\r\n cancel: { \r\n text: \"Cancel\", //default: Cancel\r\n onCancel: function () {\r\n }\r\n }\r\n };\r\n */\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'static/html/modal.html',\r\n controller: 'ModalInstanceCtrl',\r\n size: angular.isDefined(size) ? size : \"md\",\r\n resolve: {\r\n headline: function () {\r\n return headline;\r\n },\r\n message: function(){ \r\n return message;\r\n },\r\n params: function() {\r\n return params;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then(function() {\r\n \r\n }, function() {\r\n \r\n });\r\n }\r\n \r\n}\r\nModalService.$inject = [\"$uibModal\", \"$q\"];\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('ModalInstanceCtrl', ModalInstanceCtrl);\r\n\r\nfunction ModalInstanceCtrl($scope, $uibModalInstance, headline, message, params) {\r\n\r\n $scope.message = message;\r\n $scope.headline = headline;\r\n $scope.params = params;\r\n $scope.showCancel = angular.isDefined(params) && angular.isDefined(params.cancel);\r\n $scope.showNo = angular.isDefined(params) && angular.isDefined(params.no);\r\n\r\n if (angular.isUndefined(params) || angular.isUndefined(params.yes)) {\r\n $scope.params = {\r\n yes: {\r\n text: \"Ok\"\r\n }\r\n }\r\n } else if (angular.isUndefined(params.yes.text)) {\r\n params.yes.text = \"Yes\";\r\n }\r\n \r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isUndefined($scope.params.no.text)) {\r\n $scope.params.no.text = \"No\";\r\n }\r\n \r\n if (angular.isDefined(params) && angular.isDefined(params.cancel) && angular.isUndefined($scope.params.cancel.text)) {\r\n $scope.params.cancel.text = \"Cancel\";\r\n }\r\n\r\n $scope.yes = function () {\r\n $uibModalInstance.close();\r\n if(angular.isDefined(params) && angular.isDefined(params.yes) && angular.isDefined($scope.params.yes.onYes)) {\r\n $scope.params.yes.onYes();\r\n }\r\n };\r\n\r\n $scope.no = function () {\r\n $uibModalInstance.close();\r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isDefined($scope.params.no.onNo)) {\r\n $scope.params.no.onNo();\r\n }\r\n };\r\n\r\n $scope.cancel = function () {\r\n $uibModalInstance.dismiss();\r\n if (angular.isDefined(params.cancel) && angular.isDefined($scope.params.cancel.onCancel)) {\r\n $scope.params.cancel.onCancel();\r\n }\r\n };\r\n\r\n $scope.$on(\"modal.closing\", function (targetScope, reason, c) {\r\n if (reason == \"backdrop click\") {\r\n $scope.cancel();\r\n }\r\n });\r\n}\r\nModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"headline\", \"message\", \"params\"];\r\n","angular\n .module('nzbhydraApp')\n .service('GeneralModalService', GeneralModalService);\n\nfunction GeneralModalService() {\n \n \n this.open = function (msg, template, templateUrl, size, data) {\n \n //Prevent circular dependency\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\n var $uibModal = myInjector.get(\"$uibModal\");\n var params = {};\n \n if(angular.isUndefined(size)) {\n params[\"size\"] = size;\n }\n if (angular.isUndefined(template)) {\n if (angular.isUndefined(templateUrl)) {\n params[\"template\"] = '
' + msg + '
';\n } else {\n params[\"templateUrl\"] = templateUrl;\n }\n } else {\n params[\"template\"] = template;\n }\n params[\"resolve\"] = \n {\n data: function () {\n return data;\n }\n };\n \n var modalInstance = $uibModal.open(params);\n\n modalInstance.result.then();\n\n };\n \n \n}","angular\r\n .module('nzbhydraApp')\r\n .controller('LoginController', LoginController);\r\n\r\nfunction LoginController($scope, RequestsErrorHandler, $state, HydraAuthService, growl) {\r\n $scope.user = {};\r\n $scope.login = function () {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n HydraAuthService.login($scope.user.username, $scope.user.password).then(function () {\r\n HydraAuthService.setLoggedInByForm();\r\n growl.info(\"Login successful!\");\r\n $state.go(\"root.search\");\r\n }, function () {\r\n growl.error(\"Login failed!\")\r\n });\r\n });\r\n }\r\n}\r\nLoginController.$inject = [\"$scope\", \"RequestsErrorHandler\", \"$state\", \"HydraAuthService\", \"growl\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .controller('IndexerStatusesController', IndexerStatusesController);\r\n\r\n function IndexerStatusesController($scope, $http, statuses) {\r\n $scope.statuses = statuses.data.indexerStatuses;\r\n \r\n $scope.isInPast = function (timestamp) {\r\n return timestamp * 1000 < (new Date).getTime();\r\n };\r\n \r\n $scope.enable = function(indexerName) {\r\n $http.get(\"internalapi/enableindexer\", {params: {name: indexerName}}).then(function(response){\r\n $scope.statuses = response.data.indexerStatuses;\r\n });\r\n }\r\n\r\n }\r\n IndexerStatusesController.$inject = [\"$scope\", \"$http\", \"statuses\"];\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('formatDate', formatDate);\r\n\r\nfunction formatDate(dateFilter) {\r\n return function(timestamp, hidePast) {\r\n if (timestamp) {\r\n if (timestamp * 1000 < (new Date).getTime() && hidePast) {\r\n return \"\"; //\r\n }\r\n \r\n var t = timestamp * 1000;\r\n t = dateFilter(t, 'yyyy-MM-dd HH:mm');\r\n return t;\r\n } else {\r\n return \"\";\r\n }\r\n }\r\n}\r\nformatDate.$inject = [\"dateFilter\"];\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('reformatDate', reformatDate);\r\n\r\nfunction reformatDate() {\r\n return function (date) {\r\n //Date in database is saved as UTC without timezone information\r\n return moment.utc(date, \"ddd, D MMM YYYY HH:mm:ss z\").local().format(\"YYYY-MM-DD HH:mm\");\r\n \r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .controller('IndexController', IndexController);\r\n\r\nfunction IndexController($scope, $http, $stateParams, $state) {\r\n console.log(\"Index\");\r\n $state.go(\"root.search\");\r\n}\r\nIndexController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('HydraAuthService', HydraAuthService);\r\n\r\nfunction HydraAuthService($q, $rootScope, $http, $cookies, bootstrapped) {\r\n\r\n var loggedIn = bootstrapped.username;\r\n\r\n \r\n return {\r\n isLoggedIn: isLoggedIn,\r\n login: login,\r\n askForPassword: askForPassword,\r\n logout: logout,\r\n setLoggedInByForm: setLoggedInByForm,\r\n getUserRights: getUserRights,\r\n setLoggedInByBasic: setLoggedInByBasic,\r\n getUserName: getUserName,\r\n getUserInfos: getUserInfos\r\n };\r\n\r\n\r\n\r\n function getUserInfos() {\r\n return bootstrapped;\r\n }\r\n\r\n \r\n function isLoggedIn() {\r\n return bootstrapped.username;\r\n }\r\n \r\n function setLoggedInByForm() {\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n }\r\n\r\n function setLoggedInByBasic(_maySeeStats, _maySeeAdmin, _username) {\r\n }\r\n \r\n function login(username, password) {\r\n var deferred = $q.defer();\r\n return $http.post(\"auth/login\", data = {username: username, password: password}).then(function (data) {\r\n bootstrapped = data.data;\r\n loggedIn = true;\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n deferred.resolve();\r\n });\r\n return deferred;\r\n }\r\n\r\n function askForPassword(params) {\r\n return $http.get(\"internalapi/askforpassword\", {params: params}).then(function (data) {\r\n bootstrapped = data.data;\r\n return bootstrapped;\r\n });\r\n\r\n }\r\n \r\n function logout() {\r\n var deferred = $q.defer();\r\n return $http.post(\"auth/logout\").then(function(data) {\r\n $rootScope.$broadcast(\"user:loggedOut\");\r\n bootstrapped = data.data;\r\n loggedIn = false;\r\n deferred.resolve();\r\n });\r\n return deferred;\r\n }\r\n \r\n function getUserRights() {\r\n var userInfos = getUserInfos();\r\n return {maySeeStats: userInfos.maySeeStats, maySeeAdmin: userInfos.maySeeAdmin, maySeeSearch: userInfos.maySeeSearch};\r\n }\r\n \r\n function getUserName() {\r\n return bootstrapped.username;\r\n }\r\n\r\n\r\n \r\n \r\n \r\n \r\n}\r\nHydraAuthService.$inject = [\"$q\", \"$rootScope\", \"$http\", \"$cookies\", \"bootstrapped\"];","angular\r\n .module('nzbhydraApp')\r\n .controller('HeaderController', HeaderController);\r\n\r\nfunction HeaderController($scope, $state, $http, growl, HydraAuthService) {\r\n\r\n\r\n $scope.showLoginout = false;\r\n $scope.oldUserName = null;\r\n\r\n function update() {\r\n\r\n $scope.userInfos = HydraAuthService.getUserInfos();\r\n if (!$scope.userInfos.authConfigured) {\r\n $scope.showAdmin = true;\r\n $scope.showStats = true;\r\n $scope.showLoginout = false;\r\n } else {\r\n if ($scope.userInfos.username) {\r\n $scope.showAdmin = $scope.userInfos.maySeeAdmin || !$scope.userInfos.adminRestricted;\r\n $scope.showStats = $scope.userInfos.maySeeStats || !$scope.userInfos.statsRestricted;\r\n $scope.showLoginout = true;\r\n $scope.username = $scope.userInfos.username;\r\n $scope.loginlogoutText = \"Logout \" + $scope.username;\r\n $scope.oldUserName = $scope.username;\r\n } else {\r\n $scope.showAdmin = !$scope.userInfos.adminRestricted;\r\n $scope.showStats = !$scope.userInfos.statsRestricted;\r\n $scope.loginlogoutText = \"Login\";\r\n $scope.showLoginout = $scope.userInfos.adminRestricted || $scope.userInfos.statsRestricted || $scope.userInfos.searchRestricted;\r\n $scope.username = \"\";\r\n }\r\n }\r\n }\r\n\r\n update();\r\n\r\n\r\n $scope.$on(\"user:loggedIn\", function (event, data) {\r\n update();\r\n });\r\n\r\n $scope.$on(\"user:loggedOut\", function (event, data) {\r\n update();\r\n });\r\n\r\n $scope.loginout = function () {\r\n if (HydraAuthService.isLoggedIn()) {\r\n HydraAuthService.logout().then(function () {\r\n if ($scope.userInfos.authType == \"basic\") {\r\n growl.info(\"Logged out. Close your browser to make sure session is closed.\");\r\n }\r\n else if ($scope.userInfos.authType == \"form\") {\r\n growl.info(\"Logged out\");\r\n }\r\n update();\r\n //$state.go(\"root.search\", null, {reload: true});\r\n });\r\n\r\n } else {\r\n if ($scope.userInfos.authType == \"basic\") {\r\n var params = {};\r\n if ($scope.oldUserName) {\r\n params = {\r\n old_username: $scope.oldUserName\r\n }\r\n }\r\n HydraAuthService.askForPassword(params).then(function () {\r\n growl.info(\"Login successful!\");\r\n update();\r\n $scope.oldUserName = null;\r\n $state.go(\"root.search\");\r\n })\r\n } else if ($scope.userInfos.authType == \"form\") {\r\n $state.go(\"root.login\");\r\n } else {\r\n growl.info(\"You shouldn't need to login but here you go!\");\r\n }\r\n }\r\n }\r\n}\r\nHeaderController.$inject = [\"$scope\", \"$state\", \"$http\", \"growl\", \"HydraAuthService\"];\r\n","var HEADER_NAME = 'MyApp-Handle-Errors-Generically';\nvar specificallyHandleInProgress = false;\n\nnzbhydraapp.factory('RequestsErrorHandler', [\"$q\", \"growl\", \"blockUI\", \"GeneralModalService\", function ($q, growl, blockUI, GeneralModalService) {\n return {\n // --- The user's API for claiming responsiblity for requests ---\n specificallyHandled: function (specificallyHandledBlock) {\n specificallyHandleInProgress = true;\n try {\n return specificallyHandledBlock();\n } finally {\n specificallyHandleInProgress = false;\n }\n },\n\n // --- Response interceptor for handling errors generically ---\n responseError: function (rejection) {\n blockUI.reset();\n var shouldHandle = (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && !rejection.config.url.contains(\"logerror\"));\n if (shouldHandle) {\n var message = \"An error occured :
\" + rejection.status + \": \" + rejection.statusText;\n\n if (rejection.data) {\n message += \"

\" + rejection.data;\n }\n GeneralModalService.open(message);\n\n } else if (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && rejection.config.url.contains(\"logerror\")) {\n console.log(\"Not handling connection error while sending exception to server\");\n }\n\n return $q.reject(rejection);\n }\n };\n}]);\n\n\nnzbhydraapp.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {\n $httpProvider.interceptors.push('RequestsErrorHandler');\n\n // --- Decorate $http to add a special header by default ---\n\n function addHeaderToConfig(config) {\n config = config || {};\n config.headers = config.headers || {};\n\n // Add the header unless user asked to handle errors himself\n if (!specificallyHandleInProgress) {\n config.headers[HEADER_NAME] = true;\n }\n\n return config;\n }\n\n // The rest here is mostly boilerplate needed to decorate $http safely\n $provide.decorator('$http', ['$delegate', function ($delegate) {\n function decorateRegularCall(method) {\n return function (url, config) {\n return $delegate[method](url, addHeaderToConfig(config));\n };\n }\n\n function decorateDataCall(method) {\n return function (url, data, config) {\n return $delegate[method](url, data, addHeaderToConfig(config));\n };\n }\n\n function copyNotOverriddenAttributes(newHttp) {\n for (var attr in $delegate) {\n if (!newHttp.hasOwnProperty(attr)) {\n if (typeof($delegate[attr]) === 'function') {\n newHttp[attr] = function () {\n return $delegate.apply($delegate, arguments);\n };\n } else {\n newHttp[attr] = $delegate[attr];\n }\n }\n }\n }\n\n var newHttp = function (config) {\n return $delegate(addHeaderToConfig(config));\n };\n\n newHttp.get = decorateRegularCall('get');\n newHttp.delete = decorateRegularCall('delete');\n newHttp.head = decorateRegularCall('head');\n newHttp.jsonp = decorateRegularCall('jsonp');\n newHttp.post = decorateDataCall('post');\n newHttp.put = decorateDataCall('put');\n\n copyNotOverriddenAttributes(newHttp);\n\n return newHttp;\n }]);\n}]);","hashCode = function (s) {\r\n return s.split(\"\").reduce(function (a, b) {\r\n a = ((a << 5) - a) + b.charCodeAt(0);\r\n return a & a\r\n }, 0);\r\n};\r\n\r\nangular\r\n .module('nzbhydraApp').run([\"formlyConfig\", \"formlyValidationMessages\", function (formlyConfig, formlyValidationMessages) {\r\n formlyValidationMessages.addStringMessage('required', 'This field is required');\r\n formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'fc.$touched || form.$submitted';\r\n\r\n}]);\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\r\n formlyConfigProvider.extras.removeChromeAutoComplete = true;\r\n formlyConfigProvider.extras.explicitAsync = true;\r\n formlyConfigProvider.disableWarnings = window.onProd;\r\n\r\n\r\n formlyConfigProvider.setWrapper({\r\n name: 'settingWrapper',\r\n templateUrl: 'setting-wrapper.html'\r\n });\r\n\r\n\r\n formlyConfigProvider.setWrapper({\r\n name: 'fieldset',\r\n template: [\r\n '
',\r\n '{{options.templateOptions.label}}',\r\n '',\r\n '
'\r\n ].join(' ')\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'help',\r\n template: [\r\n '
',\r\n '
',\r\n '
{{ line }}
',\r\n '
',\r\n '
'\r\n ].join(' ')\r\n });\r\n\r\n\r\n formlyConfigProvider.setWrapper({\r\n name: 'logicalGroup',\r\n template: [\r\n ''\r\n ].join(' ')\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalInput',\r\n extends: 'input',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'timeOfDay',\r\n extends: 'horizontalInput',\r\n controller: ['$scope', function ($scope) {\r\n $scope.model[$scope.options.key] = moment.utc($scope.model[$scope.options.key]).toDate();\r\n }]\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'percentInput',\r\n template: [\r\n ''\r\n ].join(' ')\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'apiKeyInput',\r\n template: [\r\n '
',\r\n '',\r\n '',\r\n '',\r\n '
'\r\n ].join(' '),\r\n controller: function ($scope) {\r\n $scope.generate = function () {\r\n $scope.model[$scope.options.key] = (Math.random() * 1e32).toString(36);\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'testConnection',\r\n templateUrl: 'button-test-connection.html'\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalTestConnection',\r\n extends: 'testConnection',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'checkCaps',\r\n templateUrl: 'button-check-caps.html',\r\n controller: function ($scope, ConfigBoxService) {\r\n $scope.message = \"\";\r\n $scope.uniqueId = hashCode($scope.model.name) + hashCode($scope.model.host);\r\n\r\n var testButton = \"#button-check-caps-\" + $scope.uniqueId;\r\n var testMessage = \"#message-check-caps-\" + $scope.uniqueId;\r\n\r\n function showSuccess() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-danger\");\r\n angular.element(testButton).addClass(\"btn-success\");\r\n }\r\n\r\n function showError() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-success\");\r\n angular.element(testButton).addClass(\"btn-danger\");\r\n }\r\n\r\n $scope.checkCaps = function () {\r\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\r\n\r\n var url = \"internalapi/test_caps\";\r\n var params = {indexer: $scope.model.name, apikey: $scope.model.apikey, host: $scope.model.host};\r\n if (angular.isDefined($scope.model.username)) {\r\n params[\"username\"] = $scope.model.username;\r\n params[\"password\"] = $scope.model.password;\r\n }\r\n ConfigBoxService.checkCaps(url, params, $scope.model).then(function (data, model) {\r\n angular.element(testMessage).text(\"Supports: \" + data.supportedIds + \",\" ? data.supportedIds && data.supportedTypes : \"\" + data.supportedTypes);\r\n showSuccess();\r\n }, function (message) {\r\n angular.element(testMessage).text(message);\r\n showError();\r\n }).finally(function () {\r\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\r\n });\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalCheckCaps',\r\n extends: 'checkCaps',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalApiKeyInput',\r\n extends: 'apiKeyInput',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalPercentInput',\r\n extends: 'percentInput',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'switch',\r\n template: \r\n '
'\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'duoSetting',\r\n extends: 'input',\r\n defaultOptions: {\r\n className: 'col-md-9',\r\n templateOptions: {\r\n type: 'number',\r\n noRow: true,\r\n label: ''\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalSwitch',\r\n extends: 'switch',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalSelect',\r\n extends: 'select',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'horizontalMultiselect',\r\n defaultOptions: {\r\n templateOptions: {\r\n optionsAttr: 'bs-options',\r\n ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',\r\n valueProp: 'id',\r\n labelProp: 'label',\r\n getPlaceholder: function() {return \"\";}\r\n }\r\n },\r\n templateUrl: 'ui-select-multiple.html',\r\n wrapper: ['settingWrapper', 'bootstrapHasError']\r\n });\r\n\r\n\r\n formlyConfigProvider.setType({\r\n name: 'label',\r\n template: ''\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'duolabel',\r\n extends: 'label',\r\n defaultOptions: {\r\n className: 'col-md-2',\r\n templateOptions: {\r\n label: '-'\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'repeatSection',\r\n templateUrl: 'repeatSection.html',\r\n controller: function ($scope) {\r\n $scope.formOptions = {formState: $scope.formState};\r\n $scope.addNew = addNew;\r\n $scope.remove = remove;\r\n $scope.copyFields = copyFields;\r\n\r\n function copyFields(fields) {\r\n fields = angular.copy(fields);\r\n $scope.repeatfields = fields;\r\n return fields;\r\n }\r\n\r\n $scope.clear = function (field) {\r\n return _.mapObject(field, function (key, val) {\r\n if (typeof val === 'object') {\r\n return $scope.clear(val);\r\n }\r\n return undefined;\r\n\r\n });\r\n };\r\n\r\n\r\n function addNew() {\r\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\r\n var repeatsection = $scope.model[$scope.options.key];\r\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\r\n repeatsection.push(newsection);\r\n }\r\n\r\n function remove($index) {\r\n $scope.model[$scope.options.key].splice($index, 1);\r\n }\r\n }\r\n });\r\n\r\n formlyConfigProvider.setType({\r\n name: 'arrayConfig',\r\n templateUrl: 'arrayConfig.html',\r\n controller: function ($scope, $uibModal, growl) {\r\n $scope.formOptions = {formState: $scope.formState};\r\n $scope._showBox = _showBox;\r\n $scope.showBox = showBox;\r\n $scope.isInitial = false;\r\n\r\n $scope.presets = $scope.options.data.presets($scope.model);\r\n\r\n\r\n function _showBox(model, parentModel, isInitial, callback) {\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'configBox.html',\r\n controller: 'ConfigBoxInstanceController',\r\n size: 'lg',\r\n resolve: {\r\n model: function () {\r\n return model;\r\n },\r\n fields: function () {\r\n return $scope.options.data.fieldsFunction(model, parentModel, isInitial, angular.injector());\r\n },\r\n isInitial: function () {\r\n return isInitial\r\n },\r\n parentModel: function () {\r\n return parentModel;\r\n },\r\n data: function () {\r\n return $scope.options.data;\r\n }\r\n }\r\n });\r\n\r\n\r\n modalInstance.result.then(function () {\r\n $scope.form.$setDirty(true);\r\n if (angular.isDefined(callback)) {\r\n callback(true);\r\n }\r\n }, function () {\r\n if (angular.isDefined(callback)) {\r\n callback(false);\r\n }\r\n });\r\n }\r\n\r\n function showBox(model, parentModel) {\r\n $scope._showBox(model, parentModel, false)\r\n }\r\n\r\n $scope.addEntry = function (entriesCollection, preset) {\r\n if ($scope.options.data.checkAddingAllowed(entriesCollection, preset)) {\r\n var model = angular.copy($scope.options.data.defaultModel);\r\n if (angular.isDefined(preset)) {\r\n _.extend(model, preset);\r\n }\r\n\r\n $scope.isInitial = true;\r\n\r\n $scope._showBox(model, entriesCollection, true, function (isSubmitted) {\r\n if (isSubmitted) {\r\n entriesCollection.push(model);\r\n }\r\n });\r\n } else {\r\n growl.error(\"That predefined indexer is already configured.\"); //For now this is the only case where adding is forbidden so we use this hardcoded message \"for now\"... (;-))\r\n }\r\n\r\n };\r\n\r\n }\r\n\r\n });\r\n\r\n }]);\r\n\r\n\r\nangular.module('nzbhydraApp').controller('ConfigBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$http\", \"model\", \"fields\", \"isInitial\", \"parentModel\", \"data\", \"growl\", function ($scope, $q, $uibModalInstance, $http, model, fields, isInitial, parentModel, data, growl) {\r\n\r\n $scope.model = model;\r\n $scope.fields = fields;\r\n $scope.isInitial = isInitial;\r\n $scope.allowDelete = data.allowDeleteFunction(model);\r\n $scope.spinnerActive = false;\r\n $scope.needsConnectionTest = false;\r\n \r\n $scope.obSubmit = function () {\r\n console.log($scope);\r\n if ($scope.form.$valid) {\r\n \r\n var a = data.checkBeforeClose($scope, model).then(function() {\r\n $uibModalInstance.close($scope);\r\n });\r\n } else {\r\n growl.error(\"Config invalid. Please check your settings.\");\r\n angular.forEach($scope.form.$error, function (error) {\r\n angular.forEach(error, function (field) {\r\n field.$setTouched();\r\n });\r\n });\r\n }\r\n };\r\n\r\n $scope.reset = function () {\r\n $scope.reset();\r\n };\r\n\r\n $scope.deleteEntry = function () {\r\n parentModel.splice(parentModel.indexOf(model), 1);\r\n $uibModalInstance.close($scope);\r\n };\r\n\r\n $scope.reset = function () {\r\n if (angular.isDefined(data.resetFunction)) {\r\n data.resetFunction($scope);\r\n }\r\n };\r\n\r\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\r\n if (reason == \"backdrop click\") {\r\n $scope.reset($scope);\r\n }\r\n });\r\n}]);\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('ConfigBoxService', ConfigBoxService);\r\n\r\nfunction ConfigBoxService($http, $q) {\r\n\r\n return {\r\n checkConnection: checkConnection,\r\n checkCaps: checkCaps\r\n };\r\n\r\n function checkConnection(url, settings) {\r\n var deferred = $q.defer();\r\n\r\n $http.post(url, settings).success(function (result) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \r\n if (result.result) {\r\n deferred.resolve();\r\n } else {\r\n deferred.reject({checked: true, message: result.message});\r\n }\r\n }).error(function (result) {\r\n deferred.reject({checked: false, message: result.message});\r\n });\r\n\r\n return deferred.promise;\r\n }\r\n\r\n function checkCaps(url, params, model) {\r\n var deferred = $q.defer();\r\n\r\n $http.post(url, params).success(function (data) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \r\n if (data.success) {\r\n model.search_ids = data.supportedIds;\r\n model.searchTypes = data.supportedTypes;\r\n if (data.supportsAllCategories) { //Don't display all the categories, will be replaced with placeholder \"All categories\"\r\n model.categories = [];\r\n } else {\r\n model.categories = data.supportedCategories;\r\n }\r\n model.animeCategory = data.animeCategory;\r\n model.audiobookCategory = data.audiobookCategory;\r\n model.comicCategory = data.comicCategory;\r\n model.ebookCategory = data.ebookCategory;\r\n model.magazineCategory = data.magazineCategory;\r\n model.backend = data.backend;\r\n deferred.resolve({supportedIds: data.supportedIds, supportedTypes: data.supportedTypes}, model);\r\n } else {\r\n deferred.reject(data.message);\r\n }\r\n }).error(function () {\r\n deferred.reject(\"Unknown error\");\r\n });\r\n\r\n return deferred.promise;\r\n }\r\n\r\n}\r\nConfigBoxService.$inject = [\"$http\", \"$q\"];\r\n\r\n\r\n\r\n\r\n","var filters = angular.module('filters', []);\r\n\r\nfilters.filter('bytes', function() {\r\n\treturn function(bytes) {\r\n\t\treturn filesize(bytes);\r\n\t}\r\n});\r\n\r\nfilters.filter('unsafe', \r\n\t[\"$sce\", function ($sce) {\r\n\t\treturn function (value, type) {\r\n\t\t\treturn $sce.trustAs(type || 'html', text);\r\n\t\t};\r\n\t}]\r\n);\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('DownloaderCategoriesService', DownloaderCategoriesService);\r\n\r\nfunction DownloaderCategoriesService($http, $q, $uibModal) {\r\n\r\n var categories = {};\r\n var selectedCategory = {};\r\n\r\n var service = {\r\n get: getCategories,\r\n invalidate: invalidate,\r\n select: select,\r\n openCategorySelection: openCategorySelection\r\n };\r\n\r\n var deferred;\r\n\r\n return service;\r\n\r\n\r\n function getCategories(downloader) {\r\n\r\n function loadAll() {\r\n if (angular.isDefined(categories) && angular.isDefined(categories.downloader)) {\r\n var deferred = $q.defer();\r\n deferred.resolve(categories.downloader);\r\n return deferred.promise;\r\n }\r\n \r\n return $http.get('internalapi/getcategories', {params: {downloader: downloader.name}})\r\n .then(function (categoriesResponse) {\r\n \r\n console.log(\"Updating downloader categories cache\");\r\n var categories = {downloader: categoriesResponse.data.categories};\r\n return categoriesResponse.data.categories;\r\n\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n return loadAll().then(function (categories) {\r\n return categories;\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n\r\n function openCategorySelection(downloader) {\r\n $uibModal.open({\r\n templateUrl: 'static/html/directives/addable-nzb-modal.html',\r\n controller: 'DownloaderCategorySelectionController',\r\n size: \"sm\",\r\n resolve: {\r\n categories: function () {\r\n return getCategories(downloader)\r\n }\r\n }\r\n });\r\n deferred = $q.defer();\r\n return deferred.promise;\r\n }\r\n\r\n function select(category) {\r\n selectedCategory = category;\r\n console.log(\"Selected category \" + category);\r\n deferred.resolve(category);\r\n }\r\n\r\n function invalidate() {\r\n console.log(\"Invalidating categories\");\r\n categories = undefined;\r\n }\r\n}\r\nDownloaderCategoriesService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\r\n\r\nangular\r\n .module('nzbhydraApp').controller('DownloaderCategorySelectionController', [\"$scope\", \"$uibModalInstance\", \"DownloaderCategoriesService\", \"categories\", function ($scope, $uibModalInstance, DownloaderCategoriesService, categories) {\r\n console.log(categories);\r\n $scope.categories = categories;\r\n $scope.select = function (category) {\r\n DownloaderCategoriesService.select(category);\r\n $uibModalInstance.close($scope);\r\n }\r\n}]);","angular\n .module('nzbhydraApp')\n .controller('DownloadHistoryController', DownloadHistoryController);\n\n\nfunction DownloadHistoryController($scope, StatsService, downloads, ConfigService) {\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n $scope.sortModel = {\n column: \"time\",\n sortMode: 2\n };\n $scope.filterModel = {};\n\n //Filter options\n $scope.indexersForFiltering = [];\n _.forEach(ConfigService.getSafe().indexers, function (indexer) {\n $scope.indexersForFiltering.push({label: indexer.name, id: indexer.name})\n });\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\n $scope.successfulForFiltering = [{label: \"Succesful\", id: true}, {label: \"Unsuccesful\", id: false}, {label: \"Unknown\", id: null}];\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: false}, {label: \"Internal\", value: true}];\n\n\n //Preloaded data\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n\n\n $scope.update = function () {\n StatsService.getDownloadHistory($scope.pagination.current, $scope.limit, $scope.filterModel, $scope.sortModel).then(function (downloads) {\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n });\n };\n\n\n $scope.$on(\"sort\", function (event, column, sortMode) {\n if (sortMode == 0) {\n column = \"time\";\n sortMode = 2;\n }\n $scope.sortModel = {\n column: column,\n sortMode: sortMode\n };\n $scope.$broadcast(\"newSortColumn\", column);\n $scope.update();\n });\n\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filter) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n $scope.update();\n })\n\n}\nDownloadHistoryController.$inject = [\"$scope\", \"StatsService\", \"downloads\", \"ConfigService\"];\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateEpoch', reformatDateEpoch);\n\nfunction reformatDateEpoch() {\n return function (date) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm\");\n\n }\n}","angular\r\n .module('nzbhydraApp')\r\n .factory('ConfigService', ConfigService);\r\n\r\nfunction ConfigService($http, $q, $cacheFactory, bootstrapped) {\r\n\r\n var cache = $cacheFactory(\"nzbhydra\");\r\n var safeConfig = bootstrapped.safeConfig;\r\n\r\n return {\r\n set: set,\r\n get: get,\r\n getSafe: getSafe,\r\n invalidateSafe: invalidateSafe,\r\n maySeeAdminArea: maySeeAdminArea\r\n };\r\n\r\n\r\n function set(newConfig) {\r\n $http.put('internalapi/setsettings', newConfig)\r\n .then(function (successresponse) {\r\n console.log(\"Settings saved. Updating cache\");\r\n cache.put(\"config\", newConfig);\r\n invalidateSafe();\r\n }, function (errorresponse) {\r\n console.log(\"Error saving settings: \" + errorresponse);\r\n });\r\n }\r\n\r\n function get() {\r\n var config = cache.get(\"config\");\r\n if (angular.isUndefined(config)) {\r\n config = $http.get('internalapi/getconfig').then(function (data) {\r\n return data.data;\r\n });\r\n cache.put(\"config\", config);\r\n }\r\n\r\n return config;\r\n }\r\n\r\n function getSafe() {\r\n return safeConfig;\r\n }\r\n\r\n function invalidateSafe() {\r\n $http.get('internalapi/getsafeconfig').then(function (data) {\r\n safeConfig = data.data;\r\n });\r\n }\r\n\r\n function maySeeAdminArea() {\r\n function loadAll() {\r\n var maySeeAdminArea = cache.get(\"maySeeAdminArea\");\r\n if (!angular.isUndefined(maySeeAdminArea)) {\r\n var deferred = $q.defer();\r\n deferred.resolve(maySeeAdminArea);\r\n return deferred.promise;\r\n }\r\n\r\n return $http.get('internalapi/mayseeadminarea')\r\n .then(function (configResponse) {\r\n var config = configResponse.data;\r\n cache.put(\"maySeeAdminArea\", config);\r\n return configResponse.data;\r\n });\r\n }\r\n\r\n return loadAll().then(function (maySeeAdminArea) {\r\n return maySeeAdminArea;\r\n });\r\n }\r\n}\r\nConfigService.$inject = [\"$http\", \"$q\", \"$cacheFactory\", \"bootstrapped\"];","angular\r\n .module('nzbhydraApp')\r\n .factory('ConfigFields', ConfigFields);\r\n\r\nfunction ConfigFields($injector) {\r\n\r\n var restartWatcher;\r\n\r\n return {\r\n getFields: getFields,\r\n setRestartWatcher: setRestartWatcher\r\n };\r\n\r\n function setRestartWatcher(restartWatcherFunction) {\r\n restartWatcher = restartWatcherFunction;\r\n }\r\n\r\n\r\n function restartListener(field, newValue, oldValue) {\r\n if (newValue != oldValue) {\r\n restartWatcher();\r\n }\r\n }\r\n\r\n\r\n function ipValidator() {\r\n return {\r\n expression: function ($viewValue, $modelValue) {\r\n var value = $modelValue || $viewValue;\r\n if (value) {\r\n return /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(value)\r\n || /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(value);\r\n }\r\n return true;\r\n },\r\n message: '$viewValue + \" is not a valid IP Address\"'\r\n };\r\n }\r\n\r\n function regexValidator(regex, message, prefixViewValue) {\r\n return {\r\n expression: function ($viewValue, $modelValue) {\r\n var value = $modelValue || $viewValue;\r\n if (value) {\r\n return regex.test(value);\r\n }\r\n return true;\r\n },\r\n message: (prefixViewValue ? '$viewValue + \" ' : '\" ') + message + '\"'\r\n };\r\n }\r\n\r\n\r\n function getCategoryFields() {\r\n var fields = [];\r\n var ConfigService = $injector.get(\"ConfigService\");\r\n var categories = ConfigService.getSafe().categories;\r\n fields.push({\r\n key: 'enableCategorySizes',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Category sizes',\r\n help: \"Preset min and max sizes depending on the selected category\"\r\n }\r\n });\r\n _.each(categories, function (category) {\r\n if (category.name != \"all\" && category.name != \"na\") {\r\n var categoryFields = [\r\n {\r\n key: \"categories.\" + category.name + '.requiredWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required words',\r\n placeholder: 'separate, with, commas, like, this'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.requiredRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required regex',\r\n help: 'Must be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.forbiddenWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden words',\r\n placeholder: 'separate, with, commas, like, this'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.forbiddenRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden regex',\r\n help: 'Must not be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.applyRestrictions',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Apply restrictions',\r\n options: [\r\n {name: 'Internal searches', value: 'internal'},\r\n {name: 'API searches', value: 'external'},\r\n {name: 'All searches', value: 'both'}\r\n ],\r\n help: \"For which type of search word restrictions will be applied\"\r\n }\r\n }\r\n ];\r\n categoryFields.push({\r\n wrapper: 'settingWrapper',\r\n templateOptions: {\r\n label: 'Size preset'\r\n },\r\n fieldGroup: [\r\n {\r\n key: \"categories.\" + category.name + '.min',\r\n type: 'duoSetting',\r\n templateOptions: {\r\n addonRight: {\r\n text: 'MB'\r\n }\r\n }\r\n },\r\n {\r\n type: 'duolabel'\r\n },\r\n {\r\n key: \"categories.\" + category.name + '.max',\r\n type: 'duoSetting', templateOptions: {addonRight: {text: 'MB'}}\r\n }\r\n ]\r\n });\r\n categoryFields.push({\r\n key: \"categories.\" + category.name + '.newznabCategories',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Newznab categories',\r\n help: 'Map newznab categories to hydra categories',\r\n required: true\r\n },\r\n parsers: [function (value) {\r\n if (!value) {\r\n return value;\r\n }\r\n var arr = [];\r\n arr.push.apply(arr, value.split(\",\").map(Number));\r\n return arr;\r\n\r\n }]\r\n });\r\n categoryFields.push({\r\n key: \"categories.\" + category.name + '.ignoreResults',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Ignore results',\r\n options: [\r\n {name: 'For internal searches', value: 'internal'},\r\n {name: 'For API searches', value: 'external'},\r\n {name: 'Always', value: 'always'},\r\n {name: 'Never', value: 'never'}\r\n ],\r\n help: \"Ignore results from this category\"\r\n }\r\n });\r\n\r\n fields.push({\r\n wrapper: 'fieldset',\r\n templateOptions: {\r\n label: category.pretty\r\n },\r\n fieldGroup: categoryFields\r\n\r\n })\r\n }\r\n }\r\n );\r\n return fields;\r\n }\r\n\r\n function getFields(rootModel) {\r\n return {\r\n main: [\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Hosting'},\r\n fieldGroup: [\r\n {\r\n key: 'host',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Host',\r\n required: true,\r\n placeholder: 'IPv4/6 address to bind to',\r\n help: 'I strongly recommend using a reverse proxy instead of exposing this directly. Requires restart.'\r\n },\r\n validators: {\r\n ipAddress: ipValidator()\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'port',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Port',\r\n required: true,\r\n placeholder: '5050',\r\n help: 'Requires restart'\r\n },\r\n validators: {\r\n port: regexValidator(/^\\d{1,5}$/, \"is no valid port\", true)\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'urlBase',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'URL base',\r\n placeholder: '/nzbhydra',\r\n help: 'Set when using an external proxy. Call using a trailing slash, e.g. http://www.domain.com/nzbhydra/'\r\n },\r\n validators: {\r\n urlBase: regexValidator(/^\\/[\\w\\/]*$/, \"Base URL needs to start with a slash and must not end with one\")\r\n }\r\n },\r\n {\r\n key: 'externalUrl',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'External URL',\r\n placeholder: 'https://www.somedomain.com/nzbhydra/',\r\n help: 'Set to the full external URL so machines outside can use the generated NZB links.'\r\n }\r\n },\r\n {\r\n key: 'useLocalUrlForApiAccess',\r\n type: 'horizontalSwitch',\r\n hideExpression: '!model.externalUrl',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Use local address in API results',\r\n help: 'Disable to make API results use the external URL in NZB links.'\r\n }\r\n },\r\n {\r\n key: 'ssl',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Use SSL',\r\n help: 'I recommend using a reverse proxy instead of this. Requires restart.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'socksProxy',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'SOCKS proxy',\r\n placeholder: 'socks5://user:pass@127.0.0.1:1080',\r\n help: \"IPv4 only\"\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'httpProxy',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'HTTP proxy',\r\n placeholder: 'http://user:pass@10.0.0.1:1080',\r\n help: \"IPv4 only\"\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'httpsProxy',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'HTTPS proxy',\r\n placeholder: 'https://user:pass@10.0.0.1:1090',\r\n help: \"IPv4 only\"\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'sslcert',\r\n hideExpression: '!model.ssl',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'SSL certificate file',\r\n required: true,\r\n help: 'Requires restart.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'sslkey',\r\n hideExpression: '!model.ssl',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'SSL key file',\r\n required: true,\r\n help: 'Requires restart.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n }\r\n\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'UI'},\r\n fieldGroup: [\r\n\r\n {\r\n key: 'theme',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'Theme',\r\n help: 'Reload page after saving',\r\n options: [\r\n {name: 'Grey', value: 'grey'},\r\n {name: 'Bright', value: 'bright'},\r\n {name: 'Dark', value: 'dark'}\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Security'},\r\n fieldGroup: [\r\n\r\n {\r\n key: 'apikey',\r\n type: 'horizontalApiKeyInput',\r\n templateOptions: {\r\n label: 'API key',\r\n help: 'Remove to disable. Alphanumeric only'\r\n },\r\n validators: {\r\n apikey: regexValidator(/^[a-zA-Z0-9]*$/, \"API key must only contain numbers and digits\", false)\r\n }\r\n },\r\n {\r\n key: 'dereferer',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Dereferer',\r\n help: 'Redirect external links to hide your instance. Insert $s for target URL. Delete to disable.'\r\n }\r\n }\r\n ]\r\n },\r\n\r\n {\r\n wrapper: 'fieldset',\r\n key: 'logging',\r\n templateOptions: {label: 'Logging'},\r\n fieldGroup: [\r\n {\r\n key: 'logfilelevel',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'Logfile level',\r\n options: [\r\n {name: 'Critical', value: 'CRITICAL'},\r\n {name: 'Error', value: 'ERROR'},\r\n {name: 'Warning', value: 'WARNING'},\r\n {name: 'Info', value: 'INFO'},\r\n {name: 'Debug', value: 'DEBUG'}\r\n ]\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logfilename',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Log file',\r\n required: true\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'rolloverAtStart',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n label: 'Startup rollover',\r\n help: 'Starts a new log file on start/restart'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logMaxSize',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Max log file size',\r\n help: 'When log file size is reached a new one is started. Set to 0 to disable.',\r\n addonRight: {\r\n text: 'kB'\r\n }\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logRotateAfterDays',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Rotate after',\r\n help: 'A new log file is started after this many days. Supercedes max size. Keep empty to disable.',\r\n addonRight: {\r\n text: 'days'\r\n }\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'keepLogFiles',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Keep log files',\r\n help: 'Number of log files to keep before oldest is deleted.'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n\r\n {\r\n key: 'consolelevel',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'Console log level',\r\n options: [\r\n {name: 'Critical', value: 'CRITICAL'},\r\n {name: 'Error', value: 'ERROR'},\r\n {name: 'Warning', value: 'WARNING'},\r\n {name: 'Info', value: 'INFO'},\r\n {name: 'Debug', value: 'DEBUG'}\r\n ]\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'logIpAddresses',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Log IP addresses'\r\n }\r\n }\r\n\r\n\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Updating'},\r\n fieldGroup: [\r\n\r\n {\r\n key: 'gitPath',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n label: 'Git executable',\r\n help: 'Set if git is not in your path'\r\n }\r\n },\r\n {\r\n key: 'branch',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Repository branch',\r\n required: true,\r\n help: 'Stay on master. Seriously...'\r\n }\r\n }\r\n ]\r\n },\r\n\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {label: 'Other'},\r\n fieldGroup: [\r\n {\r\n key: 'keepSearchResultsForDays',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Store results for ...',\r\n addonRight: {\r\n text: 'days'\r\n },\r\n required: true,\r\n help: 'Meta data from searches is stored in the database. When they\\'re deleted links to hydra become invalid.'\r\n }\r\n },\r\n {\r\n key: 'debug',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Enable debugging',\r\n help: \"Only do this if you know what and why you're doing it\"\r\n }\r\n },\r\n {\r\n key: 'runThreaded',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Run threaded server',\r\n help: 'Requires restart'\r\n },\r\n watcher: {\r\n listener: restartListener\r\n }\r\n },\r\n {\r\n key: 'startupBrowser',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Open browser on startup'\r\n }\r\n },\r\n {\r\n key: 'shutdownForRestart',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Shutdown to restart',\r\n help: 'When run with a service manager which automatically restarts Hydra enable this to prevent duplicate instances'\r\n }\r\n }\r\n ]\r\n }\r\n ],\r\n\r\n searching: [\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {\r\n label: 'Indexer access'\r\n },\r\n fieldGroup: [\r\n {\r\n key: 'timeout',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Timeout when accessing indexers',\r\n addonRight: {\r\n text: 'seconds'\r\n }\r\n }\r\n },\r\n {\r\n key: 'ignoreTemporarilyDisabled',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Ignore temporarily disabled',\r\n help: \"If enabled access to indexers will never be paused after an error occurred\"\r\n }\r\n },\r\n {\r\n key: 'ignorePassworded',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Ignore passworded releases',\r\n help: \"Not all indexers provide this information\"\r\n }\r\n },\r\n {\r\n key: 'forbiddenWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden words',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: \"Results with any of these words in the title will be ignored\"\r\n }\r\n },\r\n {\r\n key: 'forbiddenRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden regex',\r\n help: 'Must not be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: 'requiredWords',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required words',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: \"Only results with at least one of these words in the title will be used\"\r\n }\r\n },\r\n {\r\n key: 'requiredRegex',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Required regex',\r\n help: 'Must be present in a title (case insensitive)'\r\n }\r\n },\r\n {\r\n key: 'applyRestrictions',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Apply word restrictions',\r\n options: [\r\n {name: 'Internal searches', value: 'internal'},\r\n {name: 'API searches', value: 'external'},\r\n {name: 'All searches', value: 'both'}\r\n ],\r\n help: \"For which type of search word restrictions will be applied\"\r\n }\r\n },\r\n {\r\n key: 'forbiddenGroups',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden groups',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: 'Posts from any groups containing any of these words will be ignored'\r\n }\r\n },\r\n {\r\n key: 'forbiddenPosters',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Forbidden posters',\r\n placeholder: 'separate, with, commas, like, this',\r\n help: 'Posts from any posters containing any of these words will be ignored'\r\n }\r\n },\r\n {\r\n key: 'maxAge',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Maximum results age',\r\n help: 'Results older than this are ignored. Can be overwritten per search',\r\n addonRight: {\r\n text: 'days'\r\n }\r\n }\r\n },\r\n {\r\n key: 'generate_queries',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Generate queries',\r\n options: [\r\n {label: 'Internal searches', id: 'internal'},\r\n {label: 'API searches', id: 'external'}\r\n ],\r\n help: \"Generate queries for indexers which do not support ID based searches\"\r\n }\r\n },\r\n {\r\n key: 'idFallbackToTitle',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Fallback to title queries',\r\n options: [\r\n {label: 'Internal searches', id: 'internal'},\r\n {label: 'API searches', id: 'external'}\r\n ],\r\n help: \"When no results were found for a query ID search again using the title\"\r\n }\r\n },\r\n {\r\n key: 'userAgent',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'User agent',\r\n required: true\r\n }\r\n }\r\n\r\n ]\r\n },\r\n {\r\n wrapper: 'fieldset',\r\n templateOptions: {\r\n label: 'Result processing'\r\n },\r\n fieldGroup: [\r\n {\r\n key: 'htmlParser',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'HTML parser',\r\n options: [\r\n {name: 'Default BS (slower)', value: 'html.parser'},\r\n {name: 'LXML (faster, needs to be installed separately)', value: 'lxml'}\r\n ]\r\n }\r\n },\r\n {\r\n key: 'duplicateSizeThresholdInPercent',\r\n type: 'horizontalPercentInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Duplicate size threshold',\r\n required: true,\r\n addonRight: {\r\n text: '%'\r\n }\r\n\r\n }\r\n },\r\n {\r\n key: 'duplicateAgeThreshold',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Duplicate age threshold',\r\n required: true,\r\n addonRight: {\r\n text: 'hours'\r\n }\r\n }\r\n },\r\n {\r\n key: 'alwaysShowDuplicates',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Always show duplicates',\r\n help: 'Activate to show duplicates in search results by default'\r\n }\r\n },\r\n {\r\n key: 'removeLanguage',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Remove language from newznab titles',\r\n help: 'Some indexers add the language to the result title, preventing proper duplicate detection'\r\n }\r\n },\r\n {\r\n key: 'removeObfuscated',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Remove \"obfuscated\" from nzbgeek titles'\r\n }\r\n },\r\n {\r\n key: 'nzbAccessType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'NZB access type',\r\n options: [\r\n {name: 'Proxy NZBs from indexer', value: 'serve'},\r\n {name: 'Redirect to the indexer', value: 'redirect'}\r\n ],\r\n help: \"How access to NZBs is provided when NZBs are downloaded (by the user or external tools). Redirecting is recommended.\"\r\n }\r\n }\r\n ]\r\n }\r\n ],\r\n\r\n categories: getCategoryFields(),\r\n\r\n downloaders: [\r\n {\r\n type: \"arrayConfig\",\r\n data: {\r\n defaultModel: {\r\n enabled: true\r\n },\r\n entryTemplateUrl: 'downloaderEntry.html',\r\n presets: function () {\r\n return getDownloaderPresets();\r\n },\r\n checkAddingAllowed: function () {\r\n return true;\r\n },\r\n presetsOnly: true,\r\n addNewText: 'Add new downloader',\r\n fieldsFunction: getDownloaderBoxFields,\r\n allowDeleteFunction: function () {\r\n return true;\r\n },\r\n checkBeforeClose: function (scope, model) {\r\n var DownloaderCheckBeforeCloseService = $injector.get(\"DownloaderCheckBeforeCloseService\");\r\n return DownloaderCheckBeforeCloseService.check(scope, model);\r\n },\r\n resetFunction: function (scope) {\r\n scope.options.resetModel();\r\n scope.options.resetModel();\r\n }\r\n\r\n }\r\n }\r\n ],\r\n\r\n\r\n indexers: [\r\n {\r\n type: \"arrayConfig\",\r\n data: {\r\n defaultModel: {\r\n animeCategory: null,\r\n comicCategory: null,\r\n audiobookCategory: null,\r\n magazineCategory: null,\r\n ebookCategory: null,\r\n enabled: true,\r\n categories: [],\r\n downloadLimit: null,\r\n host: null,\r\n apikey: null,\r\n hitLimit: null,\r\n hitLimitResetTime: 0,\r\n timeout: null,\r\n name: null,\r\n showOnSearch: true,\r\n score: 0,\r\n username: null,\r\n password: null,\r\n preselect: true,\r\n type: 'newznab',\r\n accessType: \"both\",\r\n search_ids: undefined, //[\"imdbid\", \"rid\", \"tvdbid\"],\r\n searchTypes: undefined, //[\"tvsearch\", \"movie\"]\r\n backend: 'newznab',\r\n userAgent: null\r\n },\r\n addNewText: 'Add new indexer',\r\n entryTemplateUrl: 'indexerEntry.html',\r\n presets: function (model) {\r\n return getIndexerPresets(model);\r\n },\r\n\r\n checkAddingAllowed: function (existingIndexers, preset) {\r\n if (!preset || !(preset.type == \"anizb\" || preset.type == \"binsearch\" || preset.type == \"nzbindex\" || preset.type == \"nzbclub\")) {\r\n return true;\r\n }\r\n return !_.any(existingIndexers, function (existingEntry) {\r\n return existingEntry.name == preset.name;\r\n });\r\n\r\n },\r\n fieldsFunction: getIndexerBoxFields,\r\n allowDeleteFunction: function (model) {\r\n return true;\r\n },\r\n checkBeforeClose: function (scope, model) {\r\n var IndexerCheckBeforeCloseService = $injector.get(\"IndexerCheckBeforeCloseService\");\r\n return IndexerCheckBeforeCloseService.check(scope, model);\r\n },\r\n resetFunction: function (scope) {\r\n //Then reset the model twice (for some reason when we do it once the search types / ids fields are empty, resetting again fixes that... (wtf))\r\n scope.options.resetModel();\r\n scope.options.resetModel();\r\n }\r\n\r\n }\r\n }\r\n ],\r\n\r\n auth: [\r\n {\r\n key: 'authType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Auth type',\r\n options: [\r\n {name: 'None', value: 'none'},\r\n {name: 'HTTP Basic auth', value: 'basic'},\r\n {name: 'Login form', value: 'form'}\r\n ]\r\n\r\n }\r\n },\r\n {\r\n key: 'restrictSearch',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Restrict searching',\r\n help: 'Restrict access to searching'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n key: 'restrictStats',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Restrict stats',\r\n help: 'Restrict access to stats'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n key: 'restrictAdmin',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Restrict admin',\r\n help: 'Restrict access to admin functions'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n key: 'rememberUsers',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Remember users',\r\n help: 'Remember users with cookie for 14 days'\r\n },\r\n hideExpression: function () {\r\n return rootModel.auth.authType == \"none\";\r\n }\r\n },\r\n {\r\n type: 'repeatSection',\r\n key: 'users',\r\n model: rootModel.auth,\r\n templateOptions: {\r\n btnText: 'Add new user',\r\n altLegendText: 'Authless',\r\n fields: [\r\n {\r\n key: 'username',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Username',\r\n required: true\r\n }\r\n\r\n },\r\n {\r\n key: 'password',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'password',\r\n label: 'Password',\r\n required: true\r\n }\r\n },\r\n {\r\n key: 'maySeeAdmin',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'May see admin area'\r\n }\r\n },\r\n {\r\n key: 'maySeeStats',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'May see stats'\r\n },\r\n hideExpression: 'model.maySeeAdmin'\r\n }\r\n\r\n ],\r\n defaultModel: {\r\n username: null,\r\n password: null,\r\n maySeeStats: true,\r\n maySeeAdmin: true\r\n }\r\n }\r\n }\r\n ]\r\n };\r\n }\r\n}\r\nConfigFields.$inject = [\"$injector\"];\r\n\r\n\r\nfunction getIndexerPresets(configuredIndexers) {\r\n var presets = [\r\n [\r\n {\r\n name: \"6box\",\r\n host: \"https://6box.me\"\r\n },\r\n {\r\n name: \"6box spotweb\",\r\n host: \"https://6box.me/spotweb\"\r\n },\r\n {\r\n name: \"altHUB\",\r\n host: \"https://api.althub.co.za\"\r\n },\r\n {\r\n name: \"DogNZB\",\r\n host: \"https://api.dognzb.cr\"\r\n },\r\n {\r\n name: \"Drunken Slug\",\r\n host: \"https://api.drunkenslug.com\"\r\n },\r\n {\r\n name: \"LuluNZB\",\r\n host: \"https://lulunzb.com\"\r\n },\r\n {\r\n name: \"miatrix\",\r\n host: \"https://www.miatrix.com\"\r\n },\r\n {\r\n name: \"newz69.keagaming\",\r\n host: \"https://newz69.keagaming.com\"\r\n },\r\n {\r\n name: \"NewzTown\",\r\n host: \"https://newztown.co.za\"\r\n },\r\n {\r\n name: \"NZB Finder\",\r\n host: \"https://nzbfinder.ws\"\r\n },\r\n {\r\n name: \"NZBCat\",\r\n host: \"https://nzb.cat\"\r\n },\r\n {\r\n name: \"nzb.ag\",\r\n host: \"https://nzb.ag\"\r\n },\r\n {\r\n name: \"nzb.is\",\r\n host: \"https://nzb.is\"\r\n },\r\n {\r\n name: \"nzb.su\",\r\n host: \"https://api.nzb.su\"\r\n },\r\n {\r\n name: \"nzb7\",\r\n host: \"https://www.nzb7.com\"\r\n },\r\n {\r\n name: \"NZBGeek\",\r\n host: \"https://api.nzbgeek.info\"\r\n },\r\n {\r\n name: \"NzbNdx\",\r\n host: \"https://www.nzbndx.com\"\r\n },\r\n {\r\n name: \"NzBNooB\",\r\n host: \"https://www.nzbnoob.com\"\r\n },\r\n {\r\n name: \"nzbplanet\",\r\n host: \"https://nzbplanet.net\"\r\n },\r\n {\r\n name: \"NZBs.org\",\r\n host: \"https://nzbs.org\"\r\n },\r\n {\r\n name: \"NZBs.io\",\r\n host: \"https://www.nzbs.io\"\r\n },\r\n {\r\n name: \"Nzeeb\",\r\n host: \"https://www.nzeeb.com\"\r\n },\r\n {\r\n name: \"oznzb\",\r\n host: \"https://api.oznzb.com\"\r\n },\r\n {\r\n name: \"omgwtfnzbs\",\r\n host: \"https://api.omgwtfnzbs.me\"\r\n },\r\n {\r\n name: \"PFMonkey\",\r\n host: \"https://www.pfmonkey.com\"\r\n },\r\n {\r\n name: \"SimplyNZBs\",\r\n host: \"https://simplynzbs.com\"\r\n },\r\n {\r\n name: \"Tabula-Rasa\",\r\n host: \"https://www.tabula-rasa.pw\"\r\n },\r\n {\r\n name: \"Usenet-Crawler\",\r\n host: \"https://www.usenet-crawler.com\"\r\n }\r\n ],\r\n [\r\n {\r\n name: \"Jackett/Cardigann\",\r\n host: \"http://127.0.0.1:9117/torznab/YOURTRACKER\",\r\n search_ids: [],\r\n searchTypes: [],\r\n type: \"jackett\",\r\n accessType: \"internal\"\r\n }\r\n ],\r\n [\r\n {\r\n accessType: \"both\",\r\n categories: [\"anime\"],\r\n downloadLimit: null,\r\n enabled: false,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://anizb.org\",\r\n name: \"anizb\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"anizb\",\r\n username: null\r\n },\r\n {\r\n accessType: \"internal\",\r\n categories: [],\r\n downloadLimit: null,\r\n enabled: true,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://binsearch.info\",\r\n name: \"Binsearch\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"binsearch\",\r\n username: null\r\n },\r\n {\r\n accessType: \"internal\",\r\n categories: [],\r\n downloadLimit: null,\r\n enabled: true,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://www.nzbclub.com\",\r\n name: \"NZBClub\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"nzbclub\",\r\n username: null\r\n\r\n },\r\n {\r\n accessType: \"internal\",\r\n categories: [],\r\n downloadLimit: null,\r\n enabled: true,\r\n generalMinSize: 1,\r\n hitLimit: null,\r\n hitLimitResetTime: null,\r\n host: \"https://nzbindex.com\",\r\n name: \"NZBIndex\",\r\n password: null,\r\n preselect: true,\r\n score: 0,\r\n search_ids: [],\r\n searchTypes: [],\r\n showOnSearch: true,\r\n timeout: null,\r\n type: \"nzbindex\",\r\n username: null\r\n\r\n }\r\n ]\r\n ];\r\n\r\n\r\n return presets;\r\n}\r\n\r\nfunction getIndexerBoxFields(model, parentModel, isInitial, injector) {\r\n var fieldset = [];\r\n\r\n fieldset.push({\r\n key: 'enabled',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Enabled'\r\n }\r\n });\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'name',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Name',\r\n required: true,\r\n help: 'Used for identification. Changing the name will lose all history and stats!'\r\n },\r\n validators: {\r\n uniqueName: {\r\n expression: function (viewValue) {\r\n if (isInitial || viewValue != model.name) {\r\n return _.pluck(parentModel, \"name\").indexOf(viewValue) == -1;\r\n }\r\n return true;\r\n },\r\n message: '\"Indexer \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\r\n }\r\n }\r\n })\r\n }\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'host',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Host',\r\n required: true,\r\n placeholder: 'http://www.someindexer.com'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'apikey',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'API Key'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n fieldset.push(\r\n {\r\n key: 'score',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Priority',\r\n required: true,\r\n help: 'When duplicate search results are found the result from the indexer with the highest number will be selected'\r\n }\r\n });\r\n\r\n fieldset.push(\r\n {\r\n key: 'timeout',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Timeout',\r\n help: 'Supercedes the general timeout in \"Searching\"'\r\n }\r\n });\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'hitLimit',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'API hit limit',\r\n help: 'Maximum number of API hits since \"API hit reset time\"'\r\n }\r\n },\r\n {\r\n key: 'downloadLimit',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Download limit',\r\n help: 'When # of downloads since \"Hit reset time\" is reached indexer will not be searched.'\r\n }\r\n }\r\n );\r\n fieldset.push(\r\n {\r\n key: 'hitLimitResetTime',\r\n type: 'horizontalInput',\r\n hideExpression: '!model.hitLimit && !model.downloadLimit',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Hit reset time',\r\n help: 'UTC hour of day at which the API hit counter is reset (0==24). Leave empty for a rolling reset counter'\r\n },\r\n validators: {\r\n timeOfDay: {\r\n expression: function ($viewValue, $modelValue) {\r\n var value = $modelValue || $viewValue;\r\n return value >= 0 && value <= 24;\r\n },\r\n message: '$viewValue + \" is not a valid hour of day (0-24)\"'\r\n }\r\n\r\n }\r\n });\r\n }\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'username',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n required: false,\r\n label: 'Username',\r\n help: 'Only needed if indexer requires HTTP auth for API access (rare)'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n );\r\n }\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'password',\r\n type: 'horizontalInput',\r\n hideExpression: '!model.username',\r\n templateOptions: {\r\n type: 'text',\r\n required: false,\r\n label: 'Password',\r\n help: 'Only needed if indexer requires HTTP auth for API access (rare)'\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'userAgent',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n required: false,\r\n label: 'User agent',\r\n help: 'Rarely needed. Will supercede the one in the main searching settings'\r\n }\r\n }\r\n )\r\n }\r\n\r\n\r\n fieldset.push(\r\n {\r\n key: 'preselect',\r\n type: 'horizontalSwitch',\r\n hideExpression: 'model.accessType == \"external\"',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Preselect',\r\n help: 'Preselect this indexer on the search page'\r\n }\r\n }\r\n );\r\n if (model.type != \"jackett\") {\r\n fieldset.push(\r\n {\r\n key: 'accessType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n label: 'Enable for...',\r\n options: [\r\n {name: 'Internal searches only', value: 'internal'},\r\n {name: 'API searches only', value: 'external'},\r\n {name: 'Internal and API searches', value: 'both'}\r\n ]\r\n }\r\n }\r\n );\r\n }\r\n if (model.type != \"anizb\") {\r\n fieldset.push(\r\n {\r\n key: 'categories',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Enable for...',\r\n help: 'You can decide that this indexer should only be used for certain categories',\r\n options: [\r\n {\r\n id: \"movies\",\r\n label: \"Movies\"\r\n },\r\n {\r\n id: \"movieshd\",\r\n label: \"Movies HD\"\r\n },\r\n {\r\n id: \"moviessd\",\r\n label: \"Movies SD\"\r\n },\r\n {\r\n id: \"tv\",\r\n label: \"TV\"\r\n },\r\n {\r\n id: \"tvhd\",\r\n label: \"TV HD\"\r\n },\r\n {\r\n id: \"tvsd\",\r\n label: \"TV SD\"\r\n },\r\n {\r\n id: \"anime\",\r\n label: \"Anime\"\r\n },\r\n {\r\n id: \"audio\",\r\n label: \"Audio\"\r\n },\r\n {\r\n id: \"flac\",\r\n label: \"Audio FLAC\"\r\n },\r\n {\r\n id: \"mp3\",\r\n label: \"Audio MP3\"\r\n },\r\n {\r\n id: \"audiobook\",\r\n label: \"Audiobook\"\r\n },\r\n {\r\n id: \"console\",\r\n label: \"Console\"\r\n },\r\n {\r\n id: \"pc\",\r\n label: \"PC\"\r\n },\r\n {\r\n id: \"xxx\",\r\n label: \"XXX\"\r\n },\r\n {\r\n id: \"ebook\",\r\n label: \"Ebook\"\r\n },\r\n {\r\n id: \"comic\",\r\n label: \"Comic\"\r\n }],\r\n getPlaceholder: function () {\r\n return \"All categories\";\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab') {\r\n fieldset.push(\r\n {\r\n key: 'search_ids',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Search IDs',\r\n options: [\r\n {label: 'TVDB', id: 'tvdbid'},\r\n {label: 'TVRage', id: 'rid'},\r\n {label: 'IMDB', id: 'imdbid'},\r\n {label: 'Trakt', id: 'traktid'},\r\n {label: 'TVMaze', id: 'tvmazeid'},\r\n {label: 'TMDB', id: 'tmdbid'}\r\n ],\r\n getPlaceholder: function (model) {\r\n if (angular.isUndefined(model)) {\r\n return \"Unknown\";\r\n }\r\n return \"None\";\r\n }\r\n }\r\n }\r\n );\r\n }\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n key: 'searchTypes',\r\n type: 'horizontalMultiselect',\r\n templateOptions: {\r\n label: 'Search types',\r\n options: [\r\n {label: 'Movies', id: 'movie'},\r\n {label: 'TV', id: 'tvsearch'},\r\n {label: 'Ebooks', id: 'book'},\r\n {label: 'Audio', id: 'audio'}\r\n ],\r\n getPlaceholder: function (model) {\r\n if (angular.isUndefined(model)) {\r\n return \"Unknown\";\r\n }\r\n return \"None\";\r\n }\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'newznab' || model.type == 'jackett') {\r\n fieldset.push(\r\n {\r\n type: 'horizontalCheckCaps',\r\n hideExpression: '!model.host || !model.apikey || !model.name',\r\n templateOptions: {\r\n label: 'Check capabilities',\r\n help: 'Find out what search types the indexer supports. Done automatically for new indexers.'\r\n }\r\n }\r\n )\r\n }\r\n\r\n if (model.type == 'nzbindex') {\r\n fieldset.push(\r\n {\r\n key: 'generalMinSize',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Min size',\r\n help: 'NZBIndex returns a lot of crap with small file sizes. Set this value and all smaller results will be filtered out no matter the category'\r\n }\r\n }\r\n );\r\n }\r\n\r\n return fieldset;\r\n}\r\n\r\n\r\nfunction getDownloaderBoxFields(model, parentModel, isInitial) {\r\n var fieldset = [];\r\n\r\n fieldset = _.union(fieldset, [\r\n {\r\n key: 'enabled',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Enabled'\r\n }\r\n },\r\n {\r\n key: 'name',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Name',\r\n required: true\r\n },\r\n validators: {\r\n uniqueName: {\r\n expression: function (viewValue) {\r\n if (isInitial || viewValue != model.name) {\r\n return _.pluck(parentModel, \"name\").indexOf(viewValue) == -1;\r\n }\r\n return true;\r\n },\r\n message: '\"Downloader \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\r\n }\r\n }\r\n\r\n }]);\r\n\r\n if (model.type == \"nzbget\") {\r\n fieldset = _.union(fieldset, [{\r\n key: 'host',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Host',\r\n required: true\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n\r\n },\r\n {\r\n key: 'port',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'Port',\r\n placeholder: '5050',\r\n required: true\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }, {\r\n key: 'ssl',\r\n type: 'horizontalSwitch',\r\n templateOptions: {\r\n type: 'switch',\r\n label: 'Use SSL'\r\n }\r\n }]);\r\n } else if (model.type == \"sabnzbd\") {\r\n fieldset.push({\r\n key: 'url',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'URL',\r\n required: true\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n });\r\n }\r\n fieldset = _.union(fieldset, [\r\n {\r\n key: 'username',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Username',\r\n help: model.type == \"nzbget\" ? 'Only alphanumeric usernames are guaranteed to work' : \"\"\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n },\r\n {\r\n key: 'password',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'password',\r\n label: 'Password',\r\n help: model.type == \"nzbget\" ? 'See username' : \"\"\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n }\r\n ]);\r\n\r\n\r\n if (model.type == \"sabnzbd\") {\r\n fieldset.push({\r\n key: 'apikey',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'API Key'\r\n },\r\n watcher: {\r\n listener: function (field, newValue, oldValue, scope) {\r\n if (newValue != oldValue) {\r\n scope.$parent.needsConnectionTest = true;\r\n }\r\n }\r\n }\r\n })\r\n }\r\n\r\n fieldset = _.union(fieldset, [\r\n {\r\n key: 'defaultCategory',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Default category',\r\n help: 'When adding NZBs this category will be used instead of asking for the category. Write \"No category\" to let the downloader decide.',\r\n placeholder: 'Ask when downloading'\r\n }\r\n },\r\n {\r\n key: 'nzbaccesstype',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'NZB access type',\r\n options: [\r\n {name: 'Proxy NZBs from indexer', value: 'serve'},\r\n {name: 'Redirect to the indexer', value: 'redirect'}\r\n ],\r\n help: \"How external access to NZBs is provided. Redirecting is recommended.\"\r\n }\r\n },\r\n {\r\n key: 'nzbAddingType',\r\n type: 'horizontalSelect',\r\n templateOptions: {\r\n type: 'select',\r\n label: 'NZB adding type',\r\n options: [\r\n {name: 'Send link', value: 'link'},\r\n {name: 'Upload NZB', value: 'nzb'}\r\n ],\r\n help: \"How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data\"\r\n }\r\n },\r\n {\r\n key: 'iconCssClass',\r\n type: 'horizontalInput',\r\n templateOptions: {\r\n type: 'text',\r\n label: 'Icon CSS class',\r\n help: 'Copy an icon name from http://fontawesome.io/examples/ (e.g. \"film\")',\r\n placeholder: 'Default'\r\n }\r\n }\r\n ]);\r\n\r\n return fieldset;\r\n}\r\n\r\nfunction getDownloaderPresets() {\r\n return [[\r\n {\r\n host: \"127.0.0.1\",\r\n name: \"NZBGet\",\r\n password: \"tegbzn6789x\",\r\n port: 6789,\r\n ssl: false,\r\n type: \"nzbget\",\r\n username: \"nzbgetx\",\r\n nzbAddingType: \"link\",\r\n nzbaccesstype: \"redirect\",\r\n iconCssClass: \"\",\r\n downloadType: \"nzb\"\r\n },\r\n {\r\n url: \"http://localhost:8086\",\r\n type: \"sabnzbd\",\r\n name: \"SABnzbd\",\r\n nzbAddingType: \"link\",\r\n nzbaccesstype: \"redirect\",\r\n iconCssClass: \"\",\r\n downloadType: \"nzb\",\r\n username: null,\r\n password: null\r\n }\r\n ]];\r\n}\r\n\r\n\r\nfunction handleConnectionCheckFail(ModalService, data, model, whatFailed, deferred) {\r\n var message;\r\n var yesText;\r\n if (data.checked) {\r\n message = \"The connection to the \" + whatFailed + \" failed: \" + data.message + \"
Do you want to add it anyway?\";\r\n yesText = \"I know what I'm doing\";\r\n } else {\r\n message = \"The connection to the \" + whatFailed + \" could not be tested, sorry\";\r\n yesText = \"I'll risk it\";\r\n }\r\n ModalService.open(\"Connection check failed\", message, {\r\n yes: {\r\n onYes: function () {\r\n deferred.resolve();\r\n },\r\n text: yesText\r\n },\r\n no: {\r\n onNo: function () {\r\n model.enabled = false;\r\n deferred.resolve();\r\n },\r\n text: \"Add it, but disabled\"\r\n },\r\n cancel: {\r\n onCancel: function () {\r\n deferred.reject();\r\n },\r\n text: \"Aahh, let me try again\"\r\n }\r\n });\r\n\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('IndexerCheckBeforeCloseService', IndexerCheckBeforeCloseService);\r\n\r\nfunction IndexerCheckBeforeCloseService($q, ModalService, ConfigBoxService, blockUI, growl) {\r\n\r\n return {\r\n check: checkBeforeClose\r\n };\r\n\r\n function checkBeforeClose(scope, model) {\r\n var deferred = $q.defer();\r\n if (!scope.needsConnectionTest) {\r\n checkCaps(scope, model).then(function () {\r\n deferred.resolve();\r\n }, function () {\r\n deferred.reject();\r\n });\r\n } else {\r\n blockUI.start(\"Testing connection...\");\r\n scope.spinnerActive = true;\r\n var url = \"internalapi/test_newznab\";\r\n var settings = {host: model.host, apikey: model.apikey};\r\n if (angular.isDefined(model.username)) {\r\n settings[\"username\"] = model.username;\r\n settings[\"password\"] = model.password;\r\n }\r\n ConfigBoxService.checkConnection(url, JSON.stringify(settings)).then(function () {\r\n checkCaps(scope, model).then(function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n growl.info(\"Connection to the indexer tested successfully\");\r\n deferred.resolve();\r\n }, function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n deferred.reject();\r\n });\r\n },\r\n function (data) {\r\n blockUI.reset();\r\n handleConnectionCheckFail(ModalService, data, model, \"indexer\", deferred);\r\n }).finally(function () {\r\n scope.spinnerActive = false;\r\n blockUI.reset();\r\n });\r\n }\r\n return deferred.promise;\r\n\r\n }\r\n\r\n function checkCaps(scope, model) {\r\n var deferred = $q.defer();\r\n var url = \"internalapi/test_caps\";\r\n var settings = {indexer: model.name, apikey: model.apikey, host: model.host};\r\n if (angular.isDefined(model.username)) {\r\n settings[\"username\"] = model.username;\r\n settings[\"password\"] = model.password;\r\n }\r\n if (angular.isUndefined(model.search_ids) || angular.isUndefined(model.searchTypes)) {\r\n\r\n blockUI.start(\"New indexer found. Testing its capabilities. This may take a bit...\");\r\n ConfigBoxService.checkCaps(url, JSON.stringify(settings), model).then(\r\n function (data, model) {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n growl.info(\"Successfully tested capabilites of indexer\");\r\n deferred.resolve();\r\n },\r\n function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n model.search_ids = [];\r\n model.searchTypes = [];\r\n ModalService.open(\"Error testing capabilities\", \"The capabilities of the indexer could not be checked. The indexer won't be used for ID based searches (IMDB, TVDB, etc.). You may repeat the check manually at any time.\");\r\n deferred.resolve();\r\n }).finally(\r\n function () {\r\n scope.spinnerActive = false;\r\n })\r\n } else {\r\n deferred.resolve();\r\n }\r\n return deferred.promise;\r\n\r\n }\r\n}\r\nIndexerCheckBeforeCloseService.$inject = [\"$q\", \"ModalService\", \"ConfigBoxService\", \"blockUI\", \"growl\"];\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('DownloaderCheckBeforeCloseService', DownloaderCheckBeforeCloseService);\r\n\r\nfunction DownloaderCheckBeforeCloseService($q, ConfigBoxService, growl, ModalService, blockUI) {\r\n\r\n return {\r\n check: checkBeforeClose\r\n };\r\n\r\n function checkBeforeClose(scope, model) {\r\n var deferred = $q.defer();\r\n if (!scope.isInitial && !scope.needsConnectionTest) {\r\n deferred.resolve();\r\n } else {\r\n scope.spinnerActive = true;\r\n blockUI.start(\"Testing connection...\");\r\n var url = \"internalapi/test_downloader\";\r\n ConfigBoxService.checkConnection(url, JSON.stringify(model)).then(function () {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n growl.info(\"Connection to the downloader tested successfully\");\r\n deferred.resolve();\r\n },\r\n function (data) {\r\n blockUI.reset();\r\n scope.spinnerActive = false;\r\n handleConnectionCheckFail(ModalService, data, model, \"downloader\", deferred);\r\n }).finally(function () {\r\n scope.spinnerActive = false;\r\n blockUI.reset();\r\n });\r\n }\r\n return deferred.promise;\r\n }\r\n\r\n}\r\nDownloaderCheckBeforeCloseService.$inject = [\"$q\", \"ConfigBoxService\", \"growl\", \"ModalService\", \"blockUI\"];\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('ConfigModel', function () {\r\n return {};\r\n });\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('ConfigWatcher', function () {\r\n var $scope;\r\n\r\n return {\r\n watch: watch\r\n };\r\n\r\n function watch(scope) {\r\n $scope = scope;\r\n $scope.$watchGroup([\"config.main.host\"], function () {\r\n }, true);\r\n }\r\n });\r\n\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('ConfigController', ConfigController);\r\n\r\nfunction ConfigController($scope, $http, activeTab, ConfigService, config, DownloaderCategoriesService, ConfigFields, ConfigModel, ModalService, RestartService, $state, growl) {\r\n $scope.config = config;\r\n $scope.submit = submit;\r\n $scope.activeTab = activeTab;\r\n\r\n $scope.restartRequired = false;\r\n $scope.ignoreSaveNeeded = false;\r\n\r\n ConfigFields.setRestartWatcher(function () {\r\n $scope.restartRequired = true;\r\n });\r\n \r\n\r\n function submit() {\r\n if ($scope.form.$valid) {\r\n \r\n ConfigService.set($scope.config);\r\n $scope.form.$setPristine();\r\n DownloaderCategoriesService.invalidate();\r\n if ($scope.restartRequired) {\r\n ModalService.open(\"Restart required\", \"The changes you have made may require a restart to be effective.
Do you want to restart now?\", {\r\n yes: {\r\n onYes: function () {\r\n RestartService.restart();\r\n }\r\n },\r\n no: {\r\n onNo: function () {\r\n $scope.restartRequired = false;\r\n }\r\n }\r\n });\r\n }\r\n } else {\r\n growl.error(\"Config invalid. Please check your settings.\");\r\n \r\n //Ridiculously hacky way to make the error messages appear\r\n try {\r\n if (angular.isDefined(form.$error.required)) {\r\n _.each(form.$error.required, function (item) {\r\n if (angular.isDefined(item.$error.required)) {\r\n _.each(item.$error.required, function (item2) {\r\n item2.$setTouched();\r\n });\r\n } \r\n });\r\n }\r\n angular.forEach($scope.form.$error.required, function (field) {\r\n field.$setTouched();\r\n });\r\n } catch(err) {\r\n //\r\n }\r\n \r\n }\r\n }\r\n\r\n ConfigModel = config;\r\n\r\n $scope.fields = ConfigFields.getFields($scope.config);\r\n \r\n $scope.allTabs = [\r\n {\r\n active: false,\r\n state: 'root.config.main',\r\n name: 'Main',\r\n model: ConfigModel.main,\r\n fields: $scope.fields.main\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.auth',\r\n name: 'Authorization',\r\n model: ConfigModel.auth,\r\n fields: $scope.fields.auth\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.searching',\r\n name: 'Searching',\r\n model: ConfigModel.searching,\r\n fields: $scope.fields.searching\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.categories',\r\n name: 'Categories',\r\n model: ConfigModel.categories,\r\n fields: $scope.fields.categories\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.downloader',\r\n name: 'Downloaders',\r\n model: ConfigModel.downloaders,\r\n fields: $scope.fields.downloaders\r\n },\r\n {\r\n active: false,\r\n state: 'root.config.indexers',\r\n name: 'Indexers',\r\n model: ConfigModel.indexers,\r\n fields: $scope.fields.indexers\r\n }\r\n ];\r\n\r\n $scope.isSavingNeeded = function () {\r\n return $scope.form.$dirty && $scope.form.$valid && !$scope.ignoreSaveNeeded;\r\n };\r\n\r\n $scope.goToConfigState = function (index) {\r\n $state.go($scope.allTabs[index].state, {activeTab:index}, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.help = function() {\r\n $http.get(\"internalapi/gethelp\", {params: {id: $scope.activeTab.name}}).then(function(result) {\r\n var html = '' + result.data + \"\";\r\n ModalService.open($scope.activeTab.name + \" - Help\", html, {}, \"lg\");\r\n },\r\n function() {\r\n growl.error(\"Error while loading help\")\r\n })\r\n };\r\n\r\n $scope.$on('$stateChangeStart',\r\n function (event, toState, toParams, fromState, fromParams) {\r\n if ($scope.isSavingNeeded()) {\r\n event.preventDefault();\r\n ModalService.open(\"Unsaved changed\", \"Do you want to save before leaving?\", {\r\n yes: {\r\n onYes: function() {\r\n $scope.submit();\r\n $state.go(toState);\r\n },\r\n text: \"Yes\"\r\n },\r\n no: {\r\n onNo: function () {\r\n $scope.ignoreSaveNeeded = true;\r\n $scope.ctrl.options.resetModel();\r\n $state.go(toState);\r\n },\r\n text: \"No\"\r\n },\r\n cancel: {\r\n onCancel: function () {\r\n event.preventDefault();\r\n },\r\n text: \"Cancel\"\r\n }\r\n });\r\n } \r\n })\r\n}\r\nConfigController.$inject = [\"$scope\", \"$http\", \"activeTab\", \"ConfigService\", \"config\", \"DownloaderCategoriesService\", \"ConfigFields\", \"ConfigModel\", \"ModalService\", \"RestartService\", \"$state\", \"growl\"];\r\n\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .factory('CategoriesService', CategoriesService);\r\n\r\nfunction CategoriesService(ConfigService) {\r\n\r\n return {\r\n getByName: getByName,\r\n getAll: getAll,\r\n getDefault: getDefault\r\n };\r\n\r\n\r\n function getByName(name) {\r\n for (var category in ConfigService.getSafe().categories) {\r\n category = ConfigService.getSafe().categories[category];\r\n if (category.name == name || category.pretty == name) {\r\n return category;\r\n }\r\n }\r\n }\r\n \r\n function getAll() {\r\n return ConfigService.getSafe().categories;\r\n }\r\n \r\n function getDefault() {\r\n return getAll()[1];\r\n }\r\n\r\n}\r\nCategoriesService.$inject = [\"ConfigService\"];","angular\r\n .module('nzbhydraApp')\r\n .factory('BackupService', BackupService);\r\n\r\nfunction BackupService($http) {\r\n\r\n return {\r\n getBackupsList: getBackupsList,\r\n restoreFromFile: restoreFromFile\r\n };\r\n \r\n\r\n function getBackupsList() {\r\n return $http.get('internalapi/getbackups').then(function (data) {\r\n return data.data.backups;\r\n });\r\n }\r\n\r\n function restoreFromFile(filename) {\r\n return $http.get('internalapi/restorefrombackupfile', {params:{filename: filename}}).then(function (response) {\r\n return response;\r\n });\r\n }\r\n\r\n}\r\nBackupService.$inject = [\"$http\"];"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/ui-src/js/config-fields-service.js b/ui-src/js/config-fields-service.js index c98a8f3..646b568 100644 --- a/ui-src/js/config-fields-service.js +++ b/ui-src/js/config-fields-service.js @@ -203,7 +203,7 @@ function ConfigFields($injector) { type: 'text', label: 'Host', required: true, - placeholder: 'IPv4 address to bind to', + placeholder: 'IPv4/6 address to bind to', help: 'I strongly recommend using a reverse proxy instead of exposing this directly. Requires restart.' }, validators: {