diff --git a/changelog.md b/changelog.md index a1b62e5..e6f8e39 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,12 @@ # NZB Hydra changelog ---------- +### 0.2.176 +Misc: Thank you everybody for answering the survey, I really appreciate it. Unfortunately I'm an idiot and SurveyMonkey only shows me the first 100 results for the free plan +and I'm not paying 40 euros to upgrade. So... if you were one of the first 100 your voice will be heard, otherwise not - so sorry about that. +Anyway, I'll collect what I got and especially check your comments. I'll make a collection issue on GitHub to answer all questions and requests that I can see. I might make a +new survey when I found a provider that doesn't want heaps of money... + ### 0.2.175 Added: The search page will show a popup asking you to answer a few questions in a survey. The survey is completely anonymous (I disabled IP address logging) and only about Hydra. It would help me a lot if you could participate, it will take only a few minutes. If you'd rather participate from another computer or want the URL for some reason without having to visit the page: [https://www.surveymonkey.com/r/HWXLCHM](https://www.surveymonkey.com/r/HWXLCHM) diff --git a/nzbhydra.exe b/nzbhydra.exe index 38bcea3..3aead92 100644 Binary files a/nzbhydra.exe and b/nzbhydra.exe differ diff --git a/static/js/nzbhydra.js b/static/js/nzbhydra.js index 24c5865..94f078a 100644 --- a/static/js/nzbhydra.js +++ b/static/js/nzbhydra.js @@ -2609,34 +2609,7 @@ function SearchController($scope, $http, $stateParams, $state, $window, $filter, var safeConfig = ConfigService.getSafe(); - //Doesn't belong here but whatever - if (ConfigService.getSafe().pollShown == 0) { - ModalService.open("User query", - "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!", { - yes: { - onYes: function () { - $window.open($filter("dereferer")("https://www.surveymonkey.com/r/HWXLCHM"), "_blank"); - $http.get("internalapi/pollshown", {params: {selection: 1}}); - ConfigService.getSafe().pollShown = 1; - }, - text: "Yes, I want to help. Take me there." - }, - cancel: { - onCancel: function () { - $http.get("internalapi/pollshown", {params: {selection: 0}}); - ConfigService.getSafe().pollShown = 0; - }, - text: "Not now" - }, - no: { - onNo: function () { - $http.get("internalapi/pollshown", {params: {selection: -1}}); - ConfigService.getSafe().pollShown = -1; - }, - text: "Nah, feck off" - } - }); - } + $scope.typeAheadWait = 300; diff --git a/static/js/nzbhydra.js.map b/static/js/nzbhydra.js.map index aed8bf1..636e1ad 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/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-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;ACxpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACtFA;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;ACnBA;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;AC/EA;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;AACA;ACrGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7QA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACxIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrSA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACrEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACvcA;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;ACpCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzuDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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', 'satellizer', 'LocalStorageModule', 'angular.filter', 'ngFileUpload']);\n\nangular.module('nzbhydraApp').config([\"$stateProvider\", \"$urlRouterProvider\", \"$locationProvider\", \"blockUIConfig\", \"$urlMatcherFactoryProvider\", \"$authProvider\", \"localStorageServiceProvider\", \"bootstrapped\", function ($stateProvider, $urlRouterProvider, $locationProvider, blockUIConfig, $urlMatcherFactoryProvider, $authProvider, localStorageServiceProvider, bootstrapped) {\n\n blockUIConfig.autoBlock = false;\n $urlMatcherFactoryProvider.strictMode(false);\n\n $urlRouterProvider.otherwise(\"/\");\n\n\n $stateProvider\n .state('root', {\n url: '',\n abstract: true,\n resolve: {\n //loginRequired: loginRequired\n },\n views: {\n 'header': {\n templateUrl: 'static/html/states/header.html',\n controller: 'HeaderController'\n },\n 'footer': {\n templateUrl: 'footer.html'\n }\n }\n })\n .state(\"root.config\", {\n url: \"/config\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n controllerAs: 'ctrl',\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config\"\n }]\n }\n }\n }\n })\n .state(\"root.config.auth\", {\n url: \"/auth\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Auth)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.searching\", {\n url: \"/searching\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Searching)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.categories\", {\n url: \"/categories\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Categories)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.downloader\", {\n url: \"/downloader\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Downloader)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.indexers\", {\n url: \"/indexers\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.system\", {\n url: \"/system\",\n templateUrl: \"static/html/states/config.html\",\n views: {\n 'container@': {\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System\"\n }]\n }\n }\n }\n })\n .state(\"root.config.log\", {\n url: \"/log\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Log)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats\", {\n url: \"/stats\",\n abstract: true,\n views: {\n 'container@': {\n templateUrl: \"static/html/states/stats.html\",\n controller: [\"$scope\", \"$state\", function($scope, $state) {\n $scope.$state = $state;\n }],\n resolve: {\n loginRequired: loginRequiredStats,\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n \n }\n } \n })\n .state(\"root.stats.main\", {\n url: \"/stats\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/main-stats.html\",\n controller: \"StatsController\",\n resolve: {\n loginRequired: loginRequiredStats,\n stats: ['loginRequired', 'StatsService', function (loginRequired, StatsService) {\n return StatsService.get();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.indexers\", {\n url: \"/indexers\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/indexer-statuses.html\",\n controller: IndexerStatusesController,\n resolve: {\n loginRequired: loginRequiredStats,\n statuses: [\"$http\", function($http) {\n return $http.get(\"internalapi/getindexerstatuses\").success(function (response) {\n return response.indexerStatuses;\n });\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.searches\", {\n url: \"/searches\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/search-history.html\",\n controller: SearchHistoryController,\n resolve: {\n loginRequired: loginRequiredStats,\n history: [\"StatsService\", function(StatsService) {\n return StatsService.getSearchHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Searches)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.downloads\", {\n url: \"/downloads\",\n views: {\n 'stats@root.stats': {\n templateUrl: 'static/html/states/download-history.html',\n controller: DownloadHistoryController,\n resolve: {\n loginRequired: loginRequiredStats,\n downloads: [\"StatsService\", function (StatsService) {\n return StatsService.getDownloadHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Downloads)\"\n }]\n }\n }\n }\n })\n .state(\"root.system\", {\n url: \"/system\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n askAdmin: ['loginRequired', '$http', function (loginRequired, $http) {\n return $http.get(\"internalapi/askadmin\");\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System\"\n }]\n }\n }\n }\n })\n .state(\"root.system.updates\", {\n url: \"/updates\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Updates)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.log\", {\n url: \"/log\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Log)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.backup\", {\n url: \"/backup\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Backup)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.about\", {\n url: \"/about\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (About)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.bugreport\", {\n url: \"/bugreport\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Bug report)\"\n }]\n }\n }\n }\n })\n .state(\"root.search\", {\n url: \"/?category&query&imdbid&tvdbid&title&season&episode&minsize&maxsize&minage&maxage&offsets&rid&mode&tmdbid&indexers\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/search.html\",\n controller: \"SearchController\",\n resolve: {\n loginRequired: loginRequiredSearch,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Search\";\n }]\n }\n }\n }\n })\n .state(\"root.search.results\", {\n views: {\n 'results@root.search': {\n templateUrl: \"static/html/states/search-results.html\",\n controller: \"SearchResultsController\",\n controllerAs: \"srController\",\n options: {\n inherit: true\n },\n resolve: {\n loginRequired: loginRequiredSearch,\n $title: [\"$stateParams\", function ($stateParams) {\n console.log($stateParams);\n var title = \"Search results\";\n var details; \n if ($stateParams.title) {\n details = $stateParams.title;\n } else if ($stateParams.query) {\n details = $stateParams.query;\n }\n if (details) {\n title += \" (\" + details + \")\";\n }\n return title;\n }]\n }\n }\n }\n })\n .state(\"root.login\", {\n url: \"/login\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/login.html\",\n controller: \"LoginController\",\n resolve: {\n loginRequired: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Login\"\n }]\n }\n }\n }\n })\n ;\n\n\n $locationProvider.html5Mode(true);\n\n $authProvider.httpInterceptor = function () {\n return true;\n };\n $authProvider.withCredentials = true;\n $authProvider.tokenRoot = null;\n $authProvider.baseUrl = bootstrapped.baseUrl;\n $authProvider.loginUrl = '/auth/login';\n $authProvider.signupUrl = '/auth/signup';\n $authProvider.unlinkUrl = '/unlink/';\n $authProvider.tokenName = 'token';\n $authProvider.tokenPrefix = 'satellizer';\n $authProvider.authHeader = 'TokenAuthorization';\n $authProvider.authToken = 'Bearer';\n $authProvider.storageType = 'localStorage';\n\n\n //Because I don't know for what state the login is required / asked I have a function for each \n\n function loginRequiredSearch($q, $timeout, $auth, $state, bootstrapped) {\n \n var deferred = $q.defer();\n\n if (bootstrapped.authType != \"form\" || $auth.isAuthenticated() || !bootstrapped.searchRestricted) {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n loginRequiredSearch.$inject = [\"$q\", \"$timeout\", \"$auth\", \"$state\", \"bootstrapped\"];\n\n function loginRequiredStats($q, $timeout, $auth, $state, bootstrapped) {\n var deferred = $q.defer();\n\n if (bootstrapped.authType != \"form\" || $auth.isAuthenticated() || !bootstrapped.statsRestricted) {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n loginRequiredStats.$inject = [\"$q\", \"$timeout\", \"$auth\", \"$state\", \"bootstrapped\"];\n\n function loginRequiredAdmin($q, $timeout, $auth, $state, bootstrapped) {\n var deferred = $q.defer();\n\n if (bootstrapped.authType != \"form\" || $auth.isAuthenticated() || !bootstrapped.adminRestricted) {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n loginRequiredAdmin.$inject = [\"$q\", \"$timeout\", \"$auth\", \"$state\", \"bootstrapped\"];\n\n localStorageServiceProvider\n .setPrefix('nzbhydra');\n localStorageServiceProvider\n .setNotify(true, false);\n}]);\n\n\nnzbhydraapp.config([\"paginationTemplateProvider\", function (paginationTemplateProvider) {\n paginationTemplateProvider.setPath('static/html/dirPagination.tpl.html');\n}]);\n\nnzbhydraapp.config(['cfpLoadingBarProvider', function (cfpLoadingBarProvider) {\n cfpLoadingBarProvider.latencyThreshold = 100;\n}]);\n\nnzbhydraapp.config(['growlProvider', function (growlProvider) {\n growlProvider.globalTimeToLive(5000);\n growlProvider.globalPosition('bottom-right');\n}]);\n\nnzbhydraapp.directive('ngEnter', function () {\n return function (scope, element, attr) {\n element.bind(\"keydown keypress\", function (event) {\n if (event.which === 13) {\n scope.$apply(function () {\n scope.$evalAsync(attr.ngEnter);\n });\n\n event.preventDefault();\n }\n });\n };\n});\n\nnzbhydraapp.filter('nzblink', function () {\n return function (resultItem) {\n var uri = new URI(\"internalapi/getnzb\");\n uri.addQuery(\"searchResultId\", resultItem.searchResultId);\n return uri.toString();\n }\n});\n\nnzbhydraapp.factory('focus', [\"$rootScope\", \"$timeout\", function ($rootScope, $timeout) {\n return function (name) {\n $timeout(function () {\n $rootScope.$broadcast('focusOn', name);\n });\n }\n}]);\n\nnzbhydraapp.run([\"$rootScope\", function ($rootScope) {\n $rootScope.$on('$stateChangeSuccess',\n function (event, toState, toParams, fromState, fromParams) {\n try {\n $rootScope.title = toState.views[Object.keys(toState.views)[0]].resolve.$title[1](toParams);\n } catch(e) {\n \n }\n \n });\n}]);\n\n\nnzbhydraapp.filter('unsafe', [\"$sce\", function ($sce) {\n return $sce.trustAsHtml;\n}]);\n\nnzbhydraapp.filter('dereferer', [\"ConfigService\", function (ConfigService) {\n return function(url) {\n if (ConfigService.getSafe().dereferer) {\n return ConfigService.getSafe().dereferer.replace(\"$s\", escape(url));\n }\n return url;\n }\n}]);\n\nnzbhydraapp.config([\"$provide\", function ($provide) {\n $provide.decorator(\"$exceptionHandler\", ['$delegate', '$injector', function ($delegate, $injector) {\n return function (exception, cause) {\n $delegate(exception, cause);\n try {\n console.log(exception);\n var stack = exception.stack.split('\\n').map(function (line) {\n return line.trim();\n });\n stack = stack.join(\"\\n\");\n //$injector.get(\"$http\").put(\"internalapi/logerror\", {error: stack, cause: angular.isDefined(cause) ? cause.toString() : \"No known cause\"});\n\n\n } catch (e) {\n console.error(\"Unable to log JS exception to server\", e);\n }\n };\n }]);\n}]);\n\n_.mixin({\n isNullOrEmpty: function (string) {\n return (_.isUndefined(string) || _.isNull(string) || (_.isString(string) && string.length === 0))\n }\n});\n\nnzbhydraapp.factory('sessionInjector', [\"$injector\", function ($injector) {\n var sessionInjector = {\n response: function (response) {\n if (response.headers(\"Hydra-MaySeeAdmin\") != null) {\n $injector.get(\"HydraAuthService\").setLoggedInByBasic(response.headers(\"Hydra-MaySeeStats\") == \"True\", response.headers(\"Hydra-MaySeeAdmin\") == \"True\", response.headers(\"Hydra-Username\"))\n }\n \n return response;\n }\n };\n return sessionInjector;\n}]);\n\nnzbhydraapp.config(['$httpProvider', function ($httpProvider) {\n $httpProvider.interceptors.push('sessionInjector');\n}]);\n\nnzbhydraapp.directive('autoFocus', [\"$timeout\", function ($timeout) {\n return {\n restrict: 'AC',\n link: function (_scope, _element) {\n $timeout(function () {\n _element[0].focus();\n }, 0);\n }\n };\n}]);","angular\n .module('nzbhydraApp')\n .directive('hydraupdates', hydraupdates);\n\nfunction hydraupdates() {\n controller.$inject = [\"$scope\", \"UpdateService\", \"$sce\"];\n return {\n templateUrl: 'static/html/directives/updates.html',\n controller: controller\n };\n\n function controller($scope, UpdateService, $sce) {\n\n $scope.loadingPromise = UpdateService.getVersions().then(function (data) {\n $scope.currentVersion = data.data.currentVersion;\n $scope.repVersion = data.data.repVersion;\n $scope.updateAvailable = data.data.updateAvailable;\n $scope.changelog = data.data.changelog;\n });\n \n UpdateService.getVersionHistory().then(function(data) {\n $scope.versionHistory = $sce.trustAsHtml(data.data.versionHistory);\n });\n\n $scope.update = function () {\n UpdateService.update();\n };\n\n $scope.showChangelog = function () {\n UpdateService.showChanges($scope.changelog);\n };\n \n \n\n }\n}\n\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\n .module('nzbhydraApp')\n .directive('tabOrChart', tabOrChart);\n\nfunction tabOrChart() {\n return {\n templateUrl: 'static/html/directives/tab-or-chart.html',\n transclude: {\n \"chartSlot\": \"chart\",\n \"tableSlot\": \"table\"\n },\n restrict: 'E',\n replace: true,\n scope: {\n display: \"@\"\n }\n\n };\n\n}\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\n .module('nzbhydraApp')\n .directive('otherColumns', otherColumns);\n\nfunction otherColumns($http, $templateCache, $compile, $window) {\n controller.$inject = [\"$scope\", \"$http\", \"$uibModal\", \"growl\"];\n return {\n scope: {\n result: \"<\"\n },\n multiElement: true,\n\n link: function (scope, element, attrs) {\n $http.get('static/html/directives/search-result-non-title-columns.html', {cache: $templateCache}).success(function (templateContent) {\n element.replaceWith($compile(templateContent)(scope));\n });\n\n },\n controller: controller\n };\n\n function controller($scope, $http, $uibModal, growl) {\n \n $scope.showNfo = showNfo;\n function showNfo(resultItem) {\n if (resultItem.has_nfo == 0) {\n return;\n }\n var uri = new URI(\"internalapi/getnfo\");\n uri.addQuery(\"searchresultid\", resultItem.searchResultId);\n return $http.get(uri.toString()).then(function (response) {\n if (response.data.has_nfo) {\n $scope.openModal(\"lg\", response.data.nfo)\n } else {\n if (!angular.isUndefined(resultItem.message)) {\n growl.error(resultItem.message);\n } else {\n growl.info(\"No NFO available\");\n }\n }\n });\n }\n\n $scope.openModal = openModal;\n\n function openModal(size, nfo) {\n var modalInstance = $uibModal.open({\n template: '
',\n controller: 'NfoModalInstanceCtrl',\n size: size,\n resolve: {\n nfo: function () {\n return nfo;\n }\n }\n });\n\n modalInstance.result.then();\n }\n \n $scope.downloadNzb = downloadNzb;\n \n function downloadNzb(resultItem) {\n //href = \"{{ result.link }}\"\n $window.location.href = resultItem.link;\n }\n }\n}\notherColumns.$inject = [\"$http\", \"$templateCache\", \"$compile\", \"$window\"];\n\nangular\n .module('nzbhydraApp')\n .controller('NfoModalInstanceCtrl', NfoModalInstanceCtrl);\n\nfunction NfoModalInstanceCtrl($scope, $modalInstance, nfo) {\n\n $scope.nfo = nfo;\n\n $scope.ok = function () {\n $modalInstance.close($scope.selected.item);\n };\n\n $scope.cancel = function () {\n $modalInstance.dismiss();\n };\n}\nNfoModalInstanceCtrl.$inject = [\"$scope\", \"$modalInstance\", \"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\n .module('nzbhydraApp')\n .directive('hydralog', hydralog);\n\nfunction hydralog() {\n controller.$inject = [\"$scope\", \"$http\", \"$sce\"];\n return {\n template: '
',\n controller: controller\n };\n\n function controller($scope, $http, $sce) {\n $scope.logPromise = $http.get(\"internalapi/getlogs\").success(function (data) {\n $scope.log = $sce.trustAsHtml(data.log);\n });\n\n }\n}\n\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\n .module('nzbhydraApp')\n .directive('indexerInput', indexerInput);\n\nfunction indexerInput() {\n controller.$inject = [\"$scope\"];\n return {\n templateUrl: 'static/html/directives/indexer-input.html',\n scope: {\n indexer: \"=\",\n model: \"=\",\n onClick: \"=\"\n },\n replace: true,\n controller: controller\n };\n\n function controller($scope) {\n $scope.isFocused = false;\n \n $scope.onFocus = function() {\n $scope.isFocused = true;\n };\n\n $scope.onBlur = function () {\n $scope.isFocused = false; \n };\n \n }\n}\n\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\n .module('nzbhydraApp')\n .directive('downloadNzbsButton', downloadNzbsButton);\n\nfunction downloadNzbsButton() {\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\n return {\n templateUrl: 'static/html/directives/download-nzbs-button.html',\n require: ['^searchResults'],\n scope: {\n searchResults: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService, growl) {\n\n $scope.downloaders = NzbDownloadService.getEnabledDownloaders();\n\n $scope.download = function (downloader) {\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length == 0) {\n growl.info(\"You should select at least one result...\");\n } else {\n\n var values = _.map($scope.searchResults, function (value) {\n return value.searchResultId;\n });\n\n NzbDownloadService.download(downloader, values).then(function (response) {\n if (response.data.success) {\n growl.info(\"Successfully added \" + response.data.added + \" of \" + response.data.of + \" NZBs\");\n } else {\n growl.error(\"Error while adding NZBs\");\n }\n }, function () {\n growl.error(\"Error while adding NZBs\");\n });\n }\n }\n\n\n }\n}\n\n","angular\n .module('nzbhydraApp')\n .directive('connectionTest', connectionTest);\n\nfunction connectionTest() {\n controller.$inject = [\"$scope\"];\n return {\n templateUrl: 'static/html/directives/connection-test.html',\n require: ['^type', '^data'],\n scope: {\n type: \"=\",\n id: \"=\",\n data: \"=\",\n downloader: \"=\"\n },\n controller: controller\n };\n\n function controller($scope) {\n $scope.message = \"\";\n console.log($scope);\n\n var testButton = \"#button-test-connection\";\n var testMessage = \"#message-test-connection\";\n\n function showSuccess() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).addClass(\"btn-success\");\n }\n\n function showError() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-danger\");\n }\n\n $scope.testConnection = function () {\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\n var myInjector = angular.injector([\"ng\"]);\n var $http = myInjector.get(\"$http\");\n var url;\n var params;\n if ($scope.type == \"downloader\") {\n url = \"internalapi/test_downloader\";\n params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};\n if ($scope.downloader == \"sabnzbd\") {\n params.apikey = $scope.data.apikey;\n params.url = $scope.data.url;\n } else {\n params.host = $scope.data.host;\n params.port = $scope.data.port;\n params.ssl = $scope.data.ssl;\n }\n } else if ($scope.data.type == \"newznab\") {\n url = \"internalapi/test_newznab\";\n params = {host: $scope.data.host, apikey: $scope.data.apikey};\n }\n $http.get(url, {params: params}).success(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \n if (result.result) {\n angular.element(testMessage).text(\"\");\n showSuccess();\n } else {\n angular.element(testMessage).text(result.message);\n showError();\n }\n\n }).error(function () {\n angular.element(testMessage).text(result.message);\n showError();\n }).finally(function () {\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\n })\n }\n\n }\n}\n\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\n .module('nzbhydraApp')\n .directive('addableNzbs', addableNzbs);\n\nfunction addableNzbs() {\n controller.$inject = [\"$scope\", \"NzbDownloadService\"];\n return {\n templateUrl: 'static/html/directives/addable-nzbs.html',\n require: ['^searchResultId'],\n scope: {\n searchResultId: \"<\",\n downloadType: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService) {\n $scope.downloaders = _.filter(NzbDownloadService.getEnabledDownloaders(), function(downloader) {\n if ($scope.downloadType != \"nzb\") {\n return downloader.downloadType == $scope.downloadType\n }\n return true;\n });\n }\n}\n","angular\n .module('nzbhydraApp')\n .directive('addableNzb', addableNzb);\n\nfunction addableNzb() {\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\n return {\n templateUrl: 'static/html/directives/addable-nzb.html',\n scope: {\n searchResultId: \"<\",\n downloader: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService, growl) {\n if ($scope.downloader.iconCssClass) {\n $scope.cssClass = \"fa fa-\" + $scope.downloader.iconCssClass.replace(\"fa-\",\"\").replace(\"fa \", \"\"); \n } else {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd\" : \"nzbget\";\n }\n \n $scope.add = function () {\n $scope.cssClass = \"nzb-spinning\";\n NzbDownloadService.download($scope.downloader, [$scope.searchResultId]).then(function (response) {\n if (response.data.success) {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-success\" : \"nzbget-success\";\n } else {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\n growl.error(\"Unable to add NZB. Make sure the downloader is running and properly configured.\");\n }\n }, function () {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\n growl.error(\"An unexpected error occurred while trying to contact NZB Hydra or add the NZB.\");\n })\n };\n \n \n\n }\n}\n","angular\n .module('nzbhydraApp')\n .factory('UpdateService', UpdateService);\n\nfunction UpdateService($http, growl, blockUI, RestartService) {\n\n var currentVersion;\n var repVersion;\n var updateAvailable;\n var changelog;\n var versionHistory;\n \n return {\n update: update,\n showChanges: showChanges,\n getVersions: getVersions,\n getChangelog: getChangelog,\n getVersionHistory: getVersionHistory\n };\n \n \n \n function getVersions() {\n return $http.get(\"internalapi/get_versions\").then(function (data) {\n currentVersion = data.data.currentVersion;\n repVersion = data.data.repVersion;\n updateAvailable = data.data.updateAvailable;\n return data;\n });\n }\n\n function getChangelog() {\n return $http.get(\"internalapi/get_changelog\", {currentVersion: currentVersion, repVersion: repVersion}).then(function (data) {\n changelog = data.data.changelog;\n return data;\n });\n }\n \n function getVersionHistory() {\n return $http.get(\"internalapi/get_version_history\").then(function (data) {\n versionHistory = data.data.versionHistory;\n return data;\n });\n }\n\n function showChanges(changelog) {\n\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\n var $uibModal = myInjector.get(\"$uibModal\");\n var params = {\n size: \"lg\",\n templateUrl: \"static/html/changelog.html\",\n resolve: {\n changelog: function () {\n return changelog;\n }\n },\n controller: function ($scope, $sce, $uibModalInstance, changelog) {\n //I fucking hate that untrusted HTML shit\n changelog = $sce.trustAsHtml(changelog);\n $scope.changelog = changelog;\n console.log(changelog);\n $scope.ok = function () {\n $uibModalInstance.dismiss();\n };\n }\n };\n\n var modalInstance = $uibModal.open(params);\n\n modalInstance.result.then();\n }\n \n\n function update() {\n blockUI.start(\"Updating. Please stand by...\");\n $http.get(\"internalapi/update\").then(function (data) {\n if (data.data.success) {\n RestartService.restart(\"Update complete.\", 15);\n } else {\n blockUI.reset();\n growl.info(\"An error occurred while updating. Please check the logs.\");\n }\n },\n function () {\n blockUI.reset();\n growl.info(\"An error occurred while updating. Please check the logs.\");\n });\n }\n}\nUpdateService.$inject = [\"$http\", \"growl\", \"blockUI\", \"RestartService\"];\n\n","angular\n .module('nzbhydraApp')\n .controller('UpdateFooterController', UpdateFooterController);\n\nfunction UpdateFooterController($scope, UpdateService, HydraAuthService, bootstrapped) {\n\n $scope.updateAvailable = false;\n $scope.checked = false;\n\n $scope.mayUpdate = HydraAuthService.getUserRights().maySeeAdmin || bootstrapped.maySeeAdmin;\n\n $scope.$on(\"user:loggedIn\", function (event, data) {\n if (data.maySeeAdmin && !$scope.checked) {\n retrieveUpdateInfos();\n }\n });\n\n\n if ($scope.mayUpdate) {\n retrieveUpdateInfos();\n }\n\n function retrieveUpdateInfos() {\n $scope.checked = true;\n UpdateService.getVersions().then(function (data) {\n $scope.currentVersion = data.data.currentVersion;\n $scope.repVersion = data.data.repVersion;\n $scope.updateAvailable = data.data.updateAvailable;\n $scope.changelog = data.data.changelog;\n });\n }\n\n\n $scope.update = function () {\n UpdateService.update();\n };\n\n $scope.showChangelog = function () {\n UpdateService.showChanges($scope.changelog);\n }\n\n}\nUpdateFooterController.$inject = [\"$scope\", \"UpdateService\", \"HydraAuthService\", \"bootstrapped\"];\n","angular\n .module('nzbhydraApp')\n .controller('SystemController', SystemController);\n\nfunction SystemController($scope, $state, $http, growl, RestartService, ModalService, UpdateService, NzbHydraControlService) {\n\n\n $scope.shutdown = function () {\n NzbHydraControlService.shutdown().then(function () {\n growl.info(\"Shutdown initiated. Cya!\");\n },\n function () {\n growl.info(\"Unable to send shutdown command.\");\n })\n };\n\n $scope.restart = function () {\n RestartService.restart();\n };\n\n $scope.deleteLogAndDatabase = function () {\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.\", {\n yes: {\n onYes: function () {\n NzbHydraControlService.deleteLogAndDb();\n RestartService.countdown();\n },\n text: \"Yes, delete log and database\"\n },\n no: {\n onCancel: function () {\n\n },\n text: \"Nah\"\n }\n });\n };\n\n $scope.forceUpdate = function() {\n UpdateService.update()\n };\n \n\n $scope.tabs = [\n {\n active: false,\n state: 'root.system'\n },\n {\n active: false,\n state: 'root.system.updates'\n },\n {\n active: false,\n state: 'root.system.log'\n },\n {\n active: false,\n state: 'root.system.backup'\n },\n {\n active: false,\n state: 'root.system.bugreport'\n },\n {\n active: false,\n state: 'root.system.about'\n }\n ];\n\n\n for (var i = 0; i < $scope.tabs.length; i++) {\n if ($state.is($scope.tabs[i].state)) {\n $scope.tabs[i].active = true;\n }\n }\n\n\n $scope.goToState = function (index) {\n $state.go($scope.tabs[index].state);\n \n };\n\n $scope.downloadDebuggingInfos = function() {\n $http({method: 'GET', url: 'internalapi/getdebugginginfos', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {\n var a = document.createElement('a');\n var blob = new Blob([data], {'type': \"application/octet-stream\"});\n a.href = URL.createObjectURL(blob);\n var filename = \"nzbhydra-debuginfo-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\n a.download = filename;\n \n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }).error(function (data, status, headers, config) {\n console.log(\"Error:\" + status);\n });\n }\n \n}\nSystemController.$inject = [\"$scope\", \"$state\", \"$http\", \"growl\", \"RestartService\", \"ModalService\", \"UpdateService\", \"NzbHydraControlService\"];\n","angular\n .module('nzbhydraApp')\n .factory('StatsService', StatsService);\n\nfunction StatsService($http) {\n\n return {\n get: getStats,\n getSearchHistory: getSearchHistory,\n getDownloadHistory: getDownloadHistory\n };\n\n function getStats(after, before) {\n return $http.get(\"internalapi/getstats\", {params: {after:after, before:before}}).success(function (response) {\n return response.data;\n });\n }\n\n function getSearchHistory(pageNumber, limit, type) {\n if (angular.isUndefined(pageNumber)) {\n pageNumber = 1;\n }\n if (angular.isUndefined(limit)) {\n limit = 100;\n }\n if (angular.isUndefined(type)) {\n type = \"All\";\n }\n return $http.get(\"internalapi/getsearchrequests\", {params: {page: pageNumber, limit: limit, type: type}}).success(function (response) {\n return {\n searchRequests: response.searchRequests,\n totalRequests: response.totalRequests\n }\n });\n }\n \n function getDownloadHistory(pageNumber, limit, type) {\n if (angular.isUndefined(pageNumber)) {\n pageNumber = 1;\n }\n if (angular.isUndefined(limit)) {\n limit = 100;\n }\n if (angular.isUndefined(type)) {\n type = \"All\";\n }\n console.log(1);\n return $http.get(\"internalapi/getnzbdownloads\", {params: {page: pageNumber, limit: limit, type: type}}).success(function (response) {\n console.log(2);\n return {\n nzbDownloads: response.nzbDownloads,\n totalDownloads: response.totalDownloads\n };\n \n });\n }\n\n}\nStatsService.$inject = [\"$http\"];","angular\n .module('nzbhydraApp')\n .controller('StatsController', StatsController);\n\nfunction StatsController($scope, $filter, StatsService, blockUI, stats) {\n\n $scope.dateOptions = {\n dateDisabled: false,\n formatYear: 'yy',\n startingDay: 1\n };\n\n $scope.afterDate = null;\n $scope.beforeDate = null;\n\n $scope.openAfter = function () {\n $scope.after.opened = true;\n };\n\n $scope.openBefore = function () {\n $scope.before.opened = true;\n };\n\n $scope.after = {\n opened: false\n };\n\n $scope.before = {\n opened: false\n };\n\n\n function updateStats() {\n blockUI.start(\"Updating stats...\");\n var after = $scope.afterDate != null ? $scope.afterDate.getTime() / 1000 : null;\n var before = $scope.beforeDate != null ? $scope.beforeDate.getTime() / 1000 : null;\n StatsService.get(after, before).then(function(stats) {\n $scope.setStats(stats);\n });\n\n blockUI.reset();\n }\n\n $scope.$watch('beforeDate', function () {\n updateStats();\n });\n\n $scope.$watch('afterDate', function () {\n updateStats();\n });\n\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\n $scope.format = $scope.formats[0];\n $scope.altInputFormats = ['M!/d!/yyyy'];\n\n $scope.setStats = function (stats) {\n stats = stats.data;\n\n $scope.nzbDownloads = null;\n $scope.avgResponseTimes = stats.avgResponseTimes;\n $scope.avgIndexerSearchResultsShares = stats.avgIndexerSearchResultsShares;\n $scope.avgIndexerAccessSuccesses = stats.avgIndexerAccessSuccesses;\n $scope.indexerDownloadShares = stats.indexerDownloadShares;\n $scope.downloadsPerHourOfDay = stats.timeBasedDownloadStats.perHourOfDay;\n $scope.downloadsPerDayOfWeek = stats.timeBasedDownloadStats.perDayOfWeek;\n $scope.searchesPerHourOfDay = stats.timeBasedSearchStats.perHourOfDay;\n $scope.searchesPerDayOfWeek = stats.timeBasedSearchStats.perDayOfWeek;\n\n\n var numIndexers = $scope.avgResponseTimes.length;\n\n $scope.avgResponseTimesChart = getChart(\"multiBarHorizontalChart\", $scope.avgResponseTimes, \"name\", \"avgResponseTime\", \"\", \"Response time\");\n $scope.avgResponseTimesChart.options.chart.margin.left = 100;\n $scope.avgResponseTimesChart.options.chart.yAxis.rotateLabels = -30;\n var avgResponseTimesChartHeight = Math.max(numIndexers * 30, 350);\n $scope.avgResponseTimesChart.options.chart.height = avgResponseTimesChartHeight;\n\n $scope.resultsSharesChart = getResultsSharesChart();\n\n var rotation = 30;\n if (numIndexers > 30) {\n rotation = 70;\n }\n $scope.resultsSharesChart.options.chart.xAxis.rotateLabels = rotation;\n $scope.resultsSharesChart.options.chart.height = avgResponseTimesChartHeight;\n\n $scope.downloadsPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.downloadsPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Downloads');\n $scope.downloadsPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.downloadsPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.downloadsPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Downloads');\n $scope.downloadsPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.searchesPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.searchesPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Searches');\n $scope.searchesPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.searchesPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.searchesPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Searches');\n $scope.searchesPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.indexerDownloadSharesChart = {\n options: {\n chart: {\n type: 'pieChart',\n height: 500,\n x: function (d) {\n return d.name;\n },\n y: function (d) {\n return d.share;\n },\n showLabels: true,\n duration: 500,\n labelThreshold: 0.01,\n labelSunbeamLayout: true,\n tooltip: {\n valueFormatter: function (d, i) {\n return $filter('number')(d, 2) + \"%\";\n }\n },\n legend: {\n margin: {\n top: 5,\n right: 35,\n bottom: 5,\n left: 0\n }\n }\n }\n },\n data: $scope.indexerDownloadShares\n };\n\n $scope.indexerDownloadSharesChart.options.chart.height = Math.min(Math.max(numIndexers * 40, 350), 900);\n };\n\n $scope.setStats(stats);\n\n\n function getChart(chartType, values, xKey, yKey, xAxisLabel, yAxisLabel) {\n return {\n options: {\n chart: {\n type: chartType,\n height: 350,\n margin: {\n top: 20,\n right: 20,\n bottom: 100,\n left: 50\n },\n x: function (d) {\n return d[xKey];\n },\n y: function (d) {\n return d[yKey];\n },\n showValues: true,\n valueFormat: function (d) {\n return d;\n },\n color: function () {\n return \"red\"\n },\n showControls: false,\n showLegend: false,\n duration: 100,\n xAxis: {\n axisLabel: xAxisLabel,\n tickFormat: function (d) {\n return d;\n },\n rotateLabels: 30,\n showMaxMin: false,\n color: function () {\n return \"white\"\n }\n },\n yAxis: {\n axisLabel: yAxisLabel,\n axisLabelDistance: -10,\n tickFormat: function (d) {\n return d;\n }\n },\n tooltip: {\n enabled: false\n },\n zoom: {\n enabled: true,\n scaleExtent: [1, 10],\n useFixedDomain: false,\n useNiceScale: false,\n horizontalOff: false,\n verticalOff: true,\n unzoomEventType: 'dblclick.zoom'\n }\n }\n }, data: [{\n \"key\": \"doesntmatter\",\n \"bar\": true,\n \"values\": values\n }]\n };\n }\n\n //Was unable to use the function above for this and gave up\n function getResultsSharesChart() {\n return {\n options: {\n chart: {\n type: 'multiBarChart',\n height: 350,\n margin: {\n top: 20,\n right: 20,\n bottom: 100,\n left: 45\n },\n\n clipEdge: true,\n duration: 500,\n stacked: false,\n reduceXTicks: false,\n showValues: true,\n tooltip: {\n enabled: true,\n valueFormatter: function (d) {\n return d + \"%\";\n }\n },\n showControls: false,\n xAxis: {\n axisLabel: '',\n showMaxMin: false,\n rotateLabels: 30,\n axisLabelDistance: 30,\n tickFormat: function (d) {\n return d;\n }\n },\n yAxis: {\n axisLabel: 'Share (%)',\n axisLabelDistance: -20,\n tickFormat: function (d) {\n return d;\n }\n }\n }\n },\n\n data: [\n {\n key: \"Results\",\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\n return {series: 0, y: stats.avgResultsShare, x: stats.name}\n })\n },\n {\n key: \"Unique results\",\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\n return {series: 1, y: stats.avgUniqueResults, x: stats.name}\n })\n }\n ]\n };\n }\n\n\n}\nStatsController.$inject = [\"$scope\", \"$filter\", \"StatsService\", \"blockUI\", \"stats\"];\n","//\nangular\n .module('nzbhydraApp')\n .factory('SearchService', SearchService);\n\nfunction SearchService($http) {\n\n\n var lastExecutedQuery;\n var lastResults;\n\n return {\n search: search,\n getLastResults: getLastResults,\n loadMore: loadMore\n };\n \n\n function search(category, query, tmdbid, title, tvdbid, rid, season, episode, minsize, maxsize, minage, maxage, indexers, mode) {\n var uri;\n if (category.indexOf(\"Movies\") > -1 || (category.indexOf(\"20\") == 0) || mode == \"movie\") {\n console.log(\"Search for movies\");\n uri = new URI(\"internalapi/moviesearch\");\n if (angular.isDefined(tmdbid)) {\n console.log(\"moviesearch per tmdbid\");\n uri.addQuery(\"tmdbid\", tmdbid);\n } else {\n console.log(\"moviesearch per query\");\n uri.addQuery(\"query\", query);\n }\n\n } else if (category.indexOf(\"TV\") > -1 || (category.indexOf(\"50\") == 0) || mode == \"tvsearch\") {\n console.log(\"Search for shows\");\n uri = new URI(\"internalapi/tvsearch\");\n if (angular.isDefined(tvdbid)) {\n uri.addQuery(\"tvdbid\", tvdbid);\n }\n if (angular.isDefined(rid)) {\n uri.addQuery(\"rid\", rid);\n } else {\n console.log(\"tvsearch per query\");\n uri.addQuery(\"query\", query);\n }\n\n if (angular.isDefined(season)) {\n uri.addQuery(\"season\", season);\n }\n if (angular.isDefined(episode)) {\n uri.addQuery(\"episode\", episode);\n }\n } else {\n uri = new URI(\"internalapi/search\");\n uri.addQuery(\"query\", query);\n }\n if (angular.isDefined(title)) {\n uri.addQuery(\"title\", title);\n }\n if (_.isNumber(minsize)) {\n uri.addQuery(\"minsize\", minsize);\n }\n if (_.isNumber(maxsize)) {\n uri.addQuery(\"maxsize\", maxsize);\n }\n if (_.isNumber(minage)) {\n uri.addQuery(\"minage\", minage);\n }\n if (_.isNumber(maxage)) {\n uri.addQuery(\"maxage\", maxage);\n }\n if (!angular.isUndefined(indexers)) {\n uri.addQuery(\"indexers\", decodeURIComponent(indexers));\n }\n \n\n uri.addQuery(\"category\", category);\n lastExecutedQuery = uri;\n return $http.get(uri.toString()).then(processData);\n\n }\n\n function loadMore(offset, loadAll) {\n lastExecutedQuery.removeQuery(\"offset\");\n lastExecutedQuery.addQuery(\"offset\", offset);\n lastExecutedQuery.addQuery(\"loadAll\", loadAll ? true : false);\n\n return $http.get(lastExecutedQuery.toString()).then(processData);\n }\n\n function processData(response) {\n var results = response.data.results;\n var indexersearches = response.data.indexersearches;\n var total = response.data.total;\n var rejected = response.data.rejected;\n var resultsCount = results.length;\n\n\n //Sum up response times of indexers from individual api accesses\n //TODO: Move this to search result controller because we need to update it every time we loaded more results\n _.each(indexersearches, function (ps) {\n if (ps.did_search) {\n ps.averageResponseTime = _.reduce(ps.apiAccesses, function (memo, rp) {\n return memo + rp.response_time;\n }, 0);\n ps.averageResponseTime = ps.averageResponseTime / ps.apiAccesses.length;\n }\n });\n \n lastResults = {\"results\": results, \"indexersearches\": indexersearches, \"total\": total, \"resultsCount\": resultsCount, \"rejected\": rejected};\n return lastResults;\n }\n \n function getLastResults() {\n return lastResults;\n }\n}\nSearchService.$inject = [\"$http\"];","angular\n .module('nzbhydraApp')\n .controller('SearchResultsController', SearchResultsController);\n\nfunction sumRejected(rejected) {\n return _.reduce(rejected, function (memo, entry) {\n return memo + entry[1];\n }, 0);\n}\n\n//SearchResultsController.$inject = ['blockUi'];\nfunction SearchResultsController($stateParams, $scope, $q, $timeout, blockUI, growl, localStorageService, SearchService, ConfigService) {\n\n if (localStorageService.get(\"sorting\") != null) {\n var sorting = localStorageService.get(\"sorting\");\n $scope.sortPredicate = sorting.predicate;\n $scope.sortReversed = sorting.reversed;\n } else {\n $scope.sortPredicate = \"epoch\";\n $scope.sortReversed = true;\n }\n $scope.limitTo = 100;\n $scope.offset = 0;\n //Handle incoming data\n\n $scope.indexersearches = _.sortBy(SearchService.getLastResults().indexersearches, function (i) {\n return i.indexer.toLowerCase()\n });\n $scope.indexerDisplayState = []; //Stores if a indexer's results should be displayed or not\n $scope.indexerResultsInfo = {}; //Stores information about the indexer's results like how many we already retrieved\n $scope.groupExpanded = {};\n $scope.selected = [];\n $scope.lastClicked = null;\n $scope.lastClickedValue = null;\n\n $scope.foo = {\n indexerStatusesExpanded: localStorageService.get(\"indexerStatusesExpanded\") != null ? localStorageService.get(\"indexerStatusesExpanded\") : false,\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") != null ? localStorageService.get(\"duplicatesDisplayed\") : false\n };\n\n $scope.countFilteredOut = 0;\n\n //Initially set visibility of all found indexers to true, they're needed for initial filtering / sorting\n _.forEach($scope.indexersearches, function (ps) {\n $scope.indexerDisplayState[ps.indexer.toLowerCase()] = true;\n });\n\n _.forEach($scope.indexersearches, function (ps) {\n $scope.indexerResultsInfo[ps.indexer.toLowerCase()] = {loadedResults: ps.loaded_results};\n });\n\n //Process results\n $scope.results = SearchService.getLastResults().results;\n $scope.total = SearchService.getLastResults().total;\n $scope.resultsCount = SearchService.getLastResults().resultsCount;\n $scope.rejected = SearchService.getLastResults().rejected;\n $scope.countRejected = sumRejected($scope.rejected);\n $scope.filteredResults = sortAndFilter($scope.results);\n stopBlocking();\n\n //Returns the content of the property (defined by the current sortPredicate) of the first group element \n $scope.firstResultPredicate = firstResultPredicate;\n function firstResultPredicate(item) {\n return item[0][$scope.sortPredicate];\n }\n\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...) \n $scope.groupId = groupId;\n function groupId(item) {\n return item[0][0].searchResultId;\n }\n\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?\n function startBlocking(message) {\n var deferred = $q.defer();\n blockUI.start(message);\n $timeout(function () {\n deferred.resolve();\n }, 100);\n return deferred.promise;\n }\n\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.)\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 \n $scope.setSorting = setSorting;\n function setSorting(predicate, reversedDefault) {\n if (predicate == $scope.sortPredicate) {\n $scope.sortReversed = !$scope.sortReversed;\n } else {\n $scope.sortReversed = reversedDefault;\n }\n $scope.sortPredicate = predicate;\n startBlocking(\"Sorting / filtering...\").then(function () {\n $scope.filteredResults = sortAndFilter($scope.results);\n blockUI.reset();\n localStorageService.set(\"sorting\", {predicate: predicate, reversed: $scope.sortReversed});\n });\n }\n\n $scope.inlineFilter = inlineFilter;\n function inlineFilter(result) {\n var ok = true;\n ok = ok && $scope.titleFilter && result.title.toLowerCase().indexOf($scope.titleFilter) > -1;\n ok = ok && $scope.minSizeFilter && $scope.minSizeFilter * 1024 * 1024 < result.size;\n ok = ok && $scope.maxSizeFilter && $scope.maxSizeFilter * 1024 * 1024 > result.size;\n return ok;\n }\n\n\n $scope.$on(\"searchInputChanged\", function (event, query, minage, maxage, minsize, maxsize) {\n console.log(\"Got event searchInputChanged\");\n $scope.filteredResults = sortAndFilter($scope.results, query, minage, maxage, minsize, maxsize);\n });\n\n $scope.resort = function () {\n };\n\n function sortAndFilter(results, query, minage, maxage, minsize, maxsize) {\n $scope.countFilteredOut = 0;\n\n function filterByAgeAndSize(item) {\n var ok = true;\n ok = ok && (!_.isNumber(minsize) || item.size / 1024 / 1024 >= minsize)\n && (!_.isNumber(maxsize) || item.size / 1024 / 1024 <= maxsize)\n && (!_.isNumber(minage) || item.age_days >= Number(minage))\n && (!_.isNumber(maxage) || item.age_days <= Number(maxage));\n\n if (ok && query) {\n var words = query.toLowerCase().split(\" \");\n ok = _.every(words, function (word) {\n return item.title.toLowerCase().indexOf(word) > -1;\n });\n }\n if (!ok) {\n $scope.countFilteredOut++;\n }\n return ok;\n }\n\n\n function getItemIndexerDisplayState(item) {\n return $scope.indexerDisplayState[item.indexer.toLowerCase()];\n }\n\n function getCleanedTitle(element) {\n return element.title.toLowerCase().replace(/[\\s\\-\\._]/ig, \"\");\n }\n\n function createSortedHashgroups(titleGroup) {\n\n function createHashGroup(hashGroup) {\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)\n var sortedHashGroup = _.sortBy(hashGroup, function (item) {\n var sortPredicateValue;\n if ($scope.sortPredicate == \"grabs\") {\n sortPredicateValue = angular.isDefined(item.grabs) ? item.grabs : 0;\n } else {\n sortPredicateValue = item[$scope.sortPredicate];\n }\n //var sortPredicateValue = item[$scope.sortPredicate];\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\n });\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)\n sortedHashGroup = _.sortBy(sortedHashGroup, function (item) {\n return item.indexerscore * -1;\n });\n return sortedHashGroup;\n }\n\n function getHashGroupFirstElementSortPredicate(hashGroup) {\n if ($scope.sortPredicate == \"grabs\") {\n sortPredicateValue = angular.isDefined(hashGroup[0].grabs) ? hashGroup[0].grabs : 0;\n } else {\n var sortPredicateValue = hashGroup[0][$scope.sortPredicate];\n }\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\n }\n\n return _.chain(titleGroup).groupBy(\"hash\").map(createHashGroup).sortBy(getHashGroupFirstElementSortPredicate).value();\n }\n\n function getTitleGroupFirstElementsSortPredicate(titleGroup) {\n var sortPredicateValue;\n if ($scope.sortPredicate == \"title\") {\n sortPredicateValue = titleGroup[0][0].title.toLowerCase();\n } else if ($scope.sortPredicate == \"grabs\") {\n sortPredicateValue = angular.isDefined(titleGroup[0][0].grabs) ? titleGroup[0][0].grabs : 0;\n } else {\n sortPredicateValue = titleGroup[0][0][$scope.sortPredicate];\n }\n\n return sortPredicateValue;\n }\n\n var filtered = _.chain(results)\n //Filter by age, size and title\n .filter(filterByAgeAndSize)\n //Remove elements of which the indexer is currently hidden \n .filter(getItemIndexerDisplayState)\n //Make groups of results with the same title \n .groupBy(getCleanedTitle)\n //For every title group make subgroups of duplicates and sort the group \n .map(createSortedHashgroups)\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) \n .sortBy(getTitleGroupFirstElementsSortPredicate)\n .value();\n if ($scope.sortReversed) {\n filtered = filtered.reverse();\n }\n if ($scope.countFilteredOut > 0) {\n growl.info(\"Filtered \" + $scope.countFilteredOut + \" of the retrieved results\");\n }\n\n $scope.lastClicked = null;\n return filtered;\n }\n\n $scope.toggleTitlegroupExpand = function toggleTitlegroupExpand(titleGroup) {\n $scope.groupExpanded[titleGroup[0][0].title] = !$scope.groupExpanded[titleGroup[0][0].title];\n $scope.groupExpanded[titleGroup[0][0].hash] = !$scope.groupExpanded[titleGroup[0][0].hash];\n };\n\n\n $scope.stopBlocking = stopBlocking;\n function stopBlocking() {\n blockUI.reset();\n }\n\n $scope.loadMore = loadMore;\n function loadMore(loadAll) {\n startBlocking(loadAll ? \"Loading all results...\" : \"Loading more results...\").then(function () {\n SearchService.loadMore($scope.resultsCount, loadAll).then(function (data) {\n $scope.results = $scope.results.concat(data.results);\n $scope.filteredResults = sortAndFilter($scope.results);\n $scope.total = data.total;\n $scope.rejected = data.rejected;\n $scope.countRejected = sumRejected($scope.rejected);\n $scope.resultsCount += data.resultsCount;\n stopBlocking();\n });\n });\n }\n\n\n//Filters the results according to new visibility settings.\n $scope.toggleIndexerDisplay = toggleIndexerDisplay;\n function toggleIndexerDisplay(indexer) {\n $scope.indexerDisplayState[indexer.toLowerCase()] = $scope.indexerDisplayState[indexer.toLowerCase()];\n startBlocking(\"Filtering. Sorry...\").then(function () {\n $scope.filteredResults = sortAndFilter($scope.results);\n }).then(function () {\n stopBlocking();\n });\n }\n\n $scope.countResults = countResults;\n function countResults() {\n return $scope.results.length;\n }\n\n $scope.invertSelection = function invertSelection() {\n $scope.$broadcast(\"invertSelection\");\n };\n\n $scope.toggleIndexerStatuses = function () {\n $scope.foo.indexerStatusesExpanded = !$scope.foo.indexerStatusesExpanded;\n localStorageService.set(\"indexerStatusesExpanded\", $scope.foo.indexerStatusesExpanded);\n };\n\n $scope.toggleDuplicatesDisplayed = function () {\n //$scope.foo.duplicatesDisplayed = !$scope.foo.duplicatesDisplayed;\n localStorageService.set(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\n $scope.$broadcast(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\n };\n\n $scope.$on(\"checkboxClicked\", function (event, originalEvent, rowIndex, newCheckedValue) {\n if (originalEvent.shiftKey && $scope.lastClicked != null) {\n console.log(\"Shift clicked from \" + $scope.lastClicked + \" to \" + rowIndex);\n $scope.$broadcast(\"shiftClick\", Number($scope.lastClicked), Number(rowIndex), Number($scope.lastClickedValue));\n }\n $scope.lastClicked = rowIndex;\n $scope.lastClickedValue = newCheckedValue;\n })\n\n $scope.filterRejectedZero = function() {\n return function (entry) {\n return entry[1] > 0;\n }\n }\n}\nSearchResultsController.$inject = [\"$stateParams\", \"$scope\", \"$q\", \"$timeout\", \"blockUI\", \"growl\", \"localStorageService\", \"SearchService\", \"ConfigService\"];\n\n","angular\n .module('nzbhydraApp')\n .controller('SearchHistoryController', SearchHistoryController);\n\n\nfunction SearchHistoryController($scope, $state, StatsService, history, $sce, $filter) {\n $scope.type = \"All\";\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n $scope.isLoaded = true;\n $scope.searchRequests = history.data.searchRequests;\n $scope.totalRequests = history.data.totalRequests;\n\n $scope.pageChanged = function (newPage) {\n getSearchRequestsPage(newPage);\n };\n\n $scope.changeType = function (type) {\n $scope.type = type;\n getSearchRequestsPage($scope.pagination.current);\n };\n\n function getSearchRequestsPage(pageNumber) {\n StatsService.getSearchHistory(pageNumber, $scope.limit, $scope.type).then(function (history) {\n $scope.searchRequests = history.data.searchRequests;\n $scope.totalRequests = history.data.totalRequests;\n $scope.isLoaded = true;\n });\n }\n\n $scope.openSearch = function (request) {\n var stateParams = {};\n if (request.identifier_key == \"imdbid\") {\n stateParams.imdbid = request.identifier_value;\n } else if (request.identifier_key == \"tvdbid\" || request.identifier_key == \"rid\") {\n if (request.identifier_key == \"rid\") {\n stateParams.rid = request.identifier_value;\n } else {\n stateParams.tvdbid = request.identifier_value;\n }\n\n if (request.season != \"\") {\n stateParams.season = request.season;\n }\n if (request.episode != \"\") {\n stateParams.episode = request.episode;\n }\n }\n if (request.query != \"\") {\n stateParams.query = request.query;\n }\n if (request.type == \"tv\") {\n stateParams.mode = \"tvsearch\"\n } else if (request.type == \"tv\") {\n stateParams.mode = \"movie\"\n } else {\n stateParams.mode = \"search\"\n }\n\n if (request.movietitle != null) {\n stateParams.title = request.movietitle;\n }\n if (request.tvtitle != null) {\n stateParams.title = request.tvtitle;\n }\n\n if (request.category) {\n stateParams.category = request.category;\n }\n\n stateParams.category = request.category;\n\n $state.go(\"root.search\", stateParams, {inherit: false});\n };\n\n $scope.formatQuery = function (request) {\n if (request.movietitle != null) {\n return request.movietitle;\n }\n if (request.tvtitle != null) {\n return request.tvtitle;\n }\n\n if (!request.query && !request.identifier_key && !request.season && !request.episode) {\n return \"Update query\";\n }\n return request.query;\n };\n\n $scope.formatAdditional = function(request) {\n var result = [];\n //ID key: ID value\n //season\n //episode\n //author\n //title\n if (request.identifier_key) {\n var href;\n var key;\n if (request.identifier_key == \"imdbid\") {\n key = \"IMDB ID\";\n href = \"https://www.imdb.com/title/tt\"\n } else if (request.identifier_key == \"tvdbid\") {\n key = \"TVDB ID\";\n href = \"https://thetvdb.com/?tab=series&id=\"\n } else if (request.identifier_key == \"rid\") {\n key = \"TVRage ID\";\n href = \"internalapi/redirect_rid?rid=\"\n } else if (request.identifier_key == \"tmdb\") {\n key = \"TMDV ID\";\n href = \"https://www.themoviedb.org/movie/\"\n }\n href = href + request.identifier_value;\n href = $filter(\"dereferer\")(href);\n result.push(key + \": \" + '' + request.identifier_value + \"\");\n }\n if (request.season) {\n result.push(\"Season: \" + request.season);\n }\n if (request.episode) {\n result.push(\"Episode: \" + request.episode);\n }\n if (request.author) {\n result.push(\"Author: \" + request.author);\n }\n if (request.title) {\n result.push(\"Title: \" + request.title);\n }\n return $sce.trustAsHtml(result.join(\", \"));\n };\n\n\n}\nSearchHistoryController.$inject = [\"$scope\", \"$state\", \"StatsService\", \"history\", \"$sce\", \"$filter\"];\n","angular\n .module('nzbhydraApp')\n .controller('SearchController', SearchController);\n\nfunction SearchController($scope, $http, $stateParams, $state, $window, $filter, SearchService, focus, ConfigService, CategoriesService, blockUI, $element, ModalService) {\n\n function getNumberOrUndefined(number) {\n if (_.isUndefined(number) || _.isNaN(number) || number == \"\") {\n return undefined;\n }\n number = parseInt(number);\n if (_.isNumber(number)) {\n return number;\n } else {\n return undefined;\n }\n }\n\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)\n $scope.mode = $stateParams.mode;\n $scope.categories = _.filter(CategoriesService.getAll(), function (c) {\n return c.mayBeSelected && c.ignoreResults != \"internal\" && c.ignoreResults != \"always\";\n });\n if (angular.isDefined($stateParams.category) && $stateParams.category) {\n $scope.category = CategoriesService.getByName($stateParams.category);\n } else {\n $scope.category = CategoriesService.getDefault();\n }\n $scope.category = (_.isUndefined($stateParams.category) || $stateParams.category == \"\") ? CategoriesService.getDefault() : CategoriesService.getByName($stateParams.category);\n $scope.tmdbid = $stateParams.tmdbid;\n $scope.tvdbid = $stateParams.tvdbid;\n $scope.rid = $stateParams.rid;\n $scope.title = $stateParams.title;\n $scope.season = $stateParams.season;\n $scope.episode = $stateParams.episode;\n $scope.query = $stateParams.query;\n $scope.minsize = getNumberOrUndefined($stateParams.minsize);\n $scope.maxsize = getNumberOrUndefined($stateParams.maxsize);\n $scope.minage = getNumberOrUndefined($stateParams.minage);\n $scope.maxage = getNumberOrUndefined($stateParams.maxage);\n if (!_.isUndefined($scope.title) && _.isUndefined($scope.query)) {\n //$scope.query = $scope.title;\n }\n if (!angular.isUndefined($stateParams.indexers)) {\n $scope.indexers = decodeURIComponent($stateParams.indexers).split(\"|\");\n }\n\n $scope.showIndexers = {};\n\n var safeConfig = ConfigService.getSafe();\n\n //Doesn't belong here but whatever\n if (ConfigService.getSafe().pollShown == 0) {\n ModalService.open(\"User query\",\n \"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!\", {\n yes: {\n onYes: function () {\n $window.open($filter(\"dereferer\")(\"https://www.surveymonkey.com/r/HWXLCHM\"), \"_blank\");\n $http.get(\"internalapi/pollshown\", {params: {selection: 1}});\n ConfigService.getSafe().pollShown = 1;\n },\n text: \"Yes, I want to help. Take me there.\"\n },\n cancel: {\n onCancel: function () {\n $http.get(\"internalapi/pollshown\", {params: {selection: 0}});\n ConfigService.getSafe().pollShown = 0;\n },\n text: \"Not now\"\n },\n no: {\n onNo: function () {\n $http.get(\"internalapi/pollshown\", {params: {selection: -1}});\n ConfigService.getSafe().pollShown = -1;\n },\n text: \"Nah, feck off\"\n }\n });\n }\n\n\n $scope.typeAheadWait = 300;\n $scope.selectedItem = \"\";\n $scope.autocompleteLoading = false;\n $scope.isAskById = $scope.category.supportsById;\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\n $scope.availableIndexers = [];\n $scope.autocompleteClass = \"autocompletePosterMovies\";\n\n $scope.toggle = function (searchCategory) {\n $scope.category = searchCategory;\n\n //Show checkbox to ask if the user wants to search by ID (using autocomplete)\n $scope.isAskById = $scope.category.supportsById;\n\n focus('focus-query-box');\n\n //Hacky way of triggering the autocomplete loading\n var searchModel = $element.find(\"#searchfield\").controller(\"ngModel\");\n if (angular.isDefined(searchModel.$viewValue)) {\n searchModel.$setViewValue(searchModel.$viewValue + \" \");\n }\n\n if (safeConfig.searching.enableCategorySizes) {\n var min = searchCategory.min;\n var max = searchCategory.max;\n if (_.isNumber(min)) {\n $scope.minsize = min;\n } else {\n $scope.minsize = \"\";\n }\n if (_.isNumber(max)) {\n $scope.maxsize = max;\n } else {\n $scope.maxsize = \"\";\n }\n }\n\n $scope.availableIndexers = getAvailableIndexers();\n\n\n };\n\n\n // Any function returning a promise object can be used to load values asynchronously\n $scope.getAutocomplete = function (val) {\n $scope.autocompleteLoading = true;\n //Expected model returned from API:\n //label: What to show in the results\n //title: Will be used for file search\n //value: Will be used as extraInfo (ttid oder tvdb id)\n //poster: url of poster to show\n\n //Don't use autocomplete if checkbox is disabled\n if (!$scope.isById.value) {\n return {};\n }\n\n if ($scope.category.name.indexOf(\"movies\") > -1) {\n return $http.get('internalapi/autocomplete?type=movie', {\n params: {\n input: val\n }\n }).then(function (response) {\n $scope.autocompleteLoading = false;\n return response.data.results;\n });\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\n\n return $http.get('internalapi/autocomplete?type=tv', {\n params: {\n input: val\n }\n }).then(function (response) {\n $scope.autocompleteLoading = false;\n return response.data.results;\n });\n } else {\n return {};\n }\n };\n\n\n $scope.startSearch = function () {\n blockUI.start(\"Searching...\");\n var indexers = angular.isUndefined($scope.indexers) ? undefined : $scope.indexers.join(\"|\");\n SearchService.search($scope.category.name, $scope.query, $stateParams.tmdbid, $scope.title, $scope.tvdbid, $scope.rid, $scope.season, $scope.episode, $scope.minsize, $scope.maxsize, $scope.minage, $scope.maxage, indexers, $scope.mode).then(function () {\n $state.go(\"root.search.results\", {\n minsize: $scope.minsize,\n maxsize: $scope.maxsize,\n minage: $scope.minage,\n maxage: $scope.maxage\n }, {\n inherit: true\n });\n $scope.tmdbid = undefined;\n $scope.tvdbid = undefined;\n });\n };\n\n function getSelectedIndexers() {\n var activatedIndexers = _.filter($scope.availableIndexers).filter(function (indexer) {\n return indexer.activated;\n });\n return _.pluck(activatedIndexers, \"name\").join(\"|\");\n }\n\n\n $scope.goToSearchUrl = function () {\n var stateParams = {};\n if ($scope.category.name.indexOf(\"movies\") > -1) {\n stateParams.title = $scope.title;\n stateParams.mode = \"movie\";\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\n stateParams.mode = \"tvsearch\";\n stateParams.title = $scope.title;\n } else if ($scope.category.name == \"ebook\") {\n stateParams.mode = \"ebook\";\n } else {\n stateParams.mode = \"search\";\n }\n\n stateParams.tmdbid = $scope.tmdbid;\n stateParams.tvdbid = $scope.tvdbid;\n stateParams.title = $scope.title;\n stateParams.season = $scope.season;\n stateParams.episode = $scope.episode;\n stateParams.query = $scope.query;\n stateParams.minsize = $scope.minsize;\n stateParams.maxsize = $scope.maxsize;\n stateParams.minage = $scope.minage;\n stateParams.maxage = $scope.maxage;\n stateParams.category = $scope.category.name;\n stateParams.indexers = encodeURIComponent(getSelectedIndexers());\n\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\n };\n\n\n $scope.selectAutocompleteItem = function ($item) {\n $scope.selectedItem = $item;\n $scope.title = $item.title;\n if ($scope.category.name.indexOf(\"movies\") > -1) {\n $scope.tmdbid = $item.value;\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\n $scope.tvdbid = $item.value;\n }\n $scope.query = \"\";\n $scope.goToSearchUrl();\n };\n\n $scope.startQuerySearch = function () {\n //Reset values because they might've been set from the last search\n $scope.title = undefined;\n $scope.tmdbid = undefined;\n $scope.tvdbid = undefined;\n $scope.goToSearchUrl();\n };\n\n\n $scope.autocompleteActive = function () {\n return $scope.category.supportsById;\n };\n\n $scope.seriesSelected = function () {\n return $scope.category.name.indexOf(\"tv\") > -1;\n };\n\n $scope.toggleIndexer = function (indexer) {\n $scope.indexers[indexer] = !$scope.indexers[indexer]\n };\n\n\n function isIndexerPreselected(indexer) {\n if (angular.isUndefined($scope.indexers)) {\n return indexer.preselect;\n } else {\n return _.contains($scope.indexers, indexer.name);\n }\n\n }\n\n\n function getAvailableIndexers() {\n return _.chain(safeConfig.indexers).filter(function (indexer) {\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);\n }).sortBy(function (indexer) {\n return indexer.name.toLowerCase();\n })\n .map(function (indexer) {\n return {name: indexer.name, activated: isIndexerPreselected(indexer), categories: indexer.categories};\n }).value();\n }\n\n $scope.toggleAllIndexers = function () {\n angular.forEach($scope.availableIndexers, function (indexer) {\n indexer.activated = !indexer.activated;\n })\n };\n\n $scope.searchInputChanged = function () {\n $scope.$broadcast(\"searchInputChanged\", $scope.query != $stateParams.query ? $scope.query : null, $scope.minage, $scope.maxage, $scope.minsize, $scope.maxsize);\n };\n\n $scope.availableIndexers = getAvailableIndexers();\n\n\n if ($scope.mode) {\n $scope.startSearch();\n }\n\n}\nSearchController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\", \"$window\", \"$filter\", \"SearchService\", \"focus\", \"ConfigService\", \"CategoriesService\", \"blockUI\", \"$element\", \"ModalService\"];\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\n .module('nzbhydraApp')\n .factory('NzbDownloadService', NzbDownloadService);\n\nfunction NzbDownloadService($http, ConfigService, DownloaderCategoriesService) {\n\n var service = {\n download: download,\n getEnabledDownloaders: getEnabledDownloaders\n };\n\n return service;\n\n function sendNzbAddCommand(downloader, searchresultids, category) {\n var params = {downloader: downloader.name, searchresultids: angular.toJson(searchresultids)};\n if (category != \"No category\") {\n params[\"category\"] = category;\n }\n return $http.put(\"internalapi/addnzbs\", params);\n }\n \n function download(downloader, searchresultids) {\n \n var category = downloader.defaultCategory;\n \n if ((_.isUndefined(category) || category == \"\" || category == null) && category != \"No category\") {\n return DownloaderCategoriesService.openCategorySelection(downloader).then(function (category) {\n return sendNzbAddCommand(downloader, searchresultids, category)\n }, function (error) {\n throw error;\n });\n } else {\n return sendNzbAddCommand(downloader, searchresultids, category)\n }\n }\n \n function getEnabledDownloaders() {\n return _.filter(ConfigService.getSafe().downloaders, \"enabled\");\n }\n}\nNzbDownloadService.$inject = [\"$http\", \"ConfigService\", \"DownloaderCategoriesService\"];\n\n","angular\n .module('nzbhydraApp')\n .factory('ModalService', ModalService);\n\nfunction ModalService($uibModal, $q) {\n \n return {\n open: open\n };\n \n function open(headline, message, params, size) {\n //params example:\n /*\n var p =\n {\n yes: {\n text: \"Yes\", //default: Ok\n onYes: function() {}\n },\n no: { //default: Empty\n text: \"No\",\n onNo: function () {\n }\n },\n cancel: { \n text: \"Cancel\", //default: Cancel\n onCancel: function () {\n }\n }\n };\n */\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/modal.html',\n controller: 'ModalInstanceCtrl',\n size: angular.isDefined(size) ? size : \"md\",\n resolve: {\n headline: function () {\n return headline;\n },\n message: function(){ \n return message;\n },\n params: function() {\n return params;\n }\n }\n });\n\n modalInstance.result.then(function() {\n \n }, function() {\n \n });\n }\n \n}\nModalService.$inject = [\"$uibModal\", \"$q\"];\n\nangular\n .module('nzbhydraApp')\n .controller('ModalInstanceCtrl', ModalInstanceCtrl);\n\nfunction ModalInstanceCtrl($scope, $uibModalInstance, headline, message, params) {\n\n $scope.message = message;\n $scope.headline = headline;\n $scope.params = params;\n $scope.showCancel = angular.isDefined(params) && angular.isDefined(params.cancel);\n $scope.showNo = angular.isDefined(params) && angular.isDefined(params.no);\n\n if (angular.isUndefined(params) || angular.isUndefined(params.yes)) {\n $scope.params = {\n yes: {\n text: \"Ok\"\n }\n }\n } else if (angular.isUndefined(params.yes.text)) {\n params.yes.text = \"Yes\";\n }\n \n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isUndefined($scope.params.no.text)) {\n $scope.params.no.text = \"No\";\n }\n \n if (angular.isDefined(params) && angular.isDefined(params.cancel) && angular.isUndefined($scope.params.cancel.text)) {\n $scope.params.cancel.text = \"Cancel\";\n }\n\n $scope.yes = function () {\n $uibModalInstance.close();\n if(angular.isDefined(params) && angular.isDefined(params.yes) && angular.isDefined($scope.params.yes.onYes)) {\n $scope.params.yes.onYes();\n }\n };\n\n $scope.no = function () {\n $uibModalInstance.close();\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isDefined($scope.params.no.onNo)) {\n $scope.params.no.onNo();\n }\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n if (angular.isDefined(params.cancel) && angular.isDefined($scope.params.cancel.onCancel)) {\n $scope.params.cancel.onCancel();\n }\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason, c) {\n if (reason == \"backdrop click\") {\n $scope.cancel();\n }\n });\n}\nModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"headline\", \"message\", \"params\"];\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\n .module('nzbhydraApp')\n .controller('LoginController', LoginController);\n\nfunction LoginController($scope, RequestsErrorHandler, $state, HydraAuthService, $auth, growl) {\n $scope.user = {};\n $scope.login = function () {\n RequestsErrorHandler.specificallyHandled(function () {\n $auth.login($scope.user).then(function (data) {\n HydraAuthService.setLoggedInByForm();\n growl.info(\"Login successful!\");\n $state.go(\"root.search\");\n }, function () {\n growl.error(\"Login failed!\")\n });\n });\n }\n}\nLoginController.$inject = [\"$scope\", \"RequestsErrorHandler\", \"$state\", \"HydraAuthService\", \"$auth\", \"growl\"];\n","angular\n .module('nzbhydraApp')\n .controller('IndexerStatusesController', IndexerStatusesController);\n\n function IndexerStatusesController($scope, $http, statuses) {\n $scope.statuses = statuses.data.indexerStatuses;\n \n $scope.isInPast = function (timestamp) {\n return timestamp * 1000 < (new Date).getTime();\n };\n \n $scope.enable = function(indexerName) {\n $http.get(\"internalapi/enableindexer\", {params: {name: indexerName}}).then(function(response){\n $scope.statuses = response.data.indexerStatuses;\n });\n }\n\n }\n IndexerStatusesController.$inject = [\"$scope\", \"$http\", \"statuses\"];\n\n\nangular\n .module('nzbhydraApp')\n .filter('formatDate', formatDate);\n\nfunction formatDate(dateFilter) {\n return function(timestamp, hidePast) {\n if (timestamp) {\n if (timestamp * 1000 < (new Date).getTime() && hidePast) {\n return \"\"; //\n }\n \n var t = timestamp * 1000;\n t = dateFilter(t, 'yyyy-MM-dd HH:mm');\n return t;\n } else {\n return \"\";\n }\n }\n}\nformatDate.$inject = [\"dateFilter\"];\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDate', reformatDate);\n\nfunction reformatDate() {\n return function (date) {\n //Date in database is saved as UTC without timezone information\n return moment.utc(date, \"ddd, D MMM YYYY HH:mm:ss z\").local().format(\"YYYY-MM-DD HH:mm\");\n \n }\n}","angular\n .module('nzbhydraApp')\n .controller('IndexController', IndexController);\n\nfunction IndexController($scope, $http, $stateParams, $state) {\n console.log(\"Index\");\n $state.go(\"root.search\");\n}\nIndexController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\"];\n","angular\n .module('nzbhydraApp')\n .factory('HydraAuthService', HydraAuthService);\n\nfunction HydraAuthService($auth, $q, $rootScope, ConfigService, bootstrapped) {\n\n var loggedIn = false;\n var username;\n var maySeeAdmin = bootstrapped.maySeeAdmin;\n var maySeeStats = bootstrapped.maySeeStats;\n \n return {\n isLoggedIn: isLoggedIn,\n login: login,\n logout: logout,\n setLoggedInByForm: setLoggedInByForm,\n getUserRights: getUserRights,\n setLoggedInByBasic: setLoggedInByBasic,\n getUserName: getUserName\n };\n \n function isLoggedIn() {\n return loggedIn || (ConfigService.getSafe().authType == \"form\" && $auth.isAuthenticated()) || ConfigService.getSafe().authType == \"none\";\n }\n \n function setLoggedInByForm() {\n maySeeStats = $auth.getPayload().maySeeStats;\n maySeeAdmin = $auth.getPayload().maySeeAdmin;\n username = $auth.getPayload().username;\n loggedIn = true;\n $rootScope.$broadcast(\"user:loggedIn\", {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin});\n }\n\n function setLoggedInByBasic(_maySeeStats, _maySeeAdmin, _username) {\n maySeeAdmin = _maySeeAdmin;\n maySeeStats = _maySeeStats;\n username = _username;\n loggedIn = true;\n $rootScope.$broadcast(\"user:loggedIn\", {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin});\n }\n \n function login(user) {\n var deferred = $q.defer();\n $auth.login(user).then(function (data) {\n \n $rootScope.$broadcast(\"user:loggedIn\", data);\n deferred.resolve();\n });\n return deferred;\n }\n \n function logout() {\n $auth.logout();\n loggedIn = false;\n $rootScope.$broadcast(\"user:loggedOut\");\n }\n \n function getUserRights() {\n return {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin};\n }\n \n function getUserName() {\n return username;\n }\n \n \n \n \n}\nHydraAuthService.$inject = [\"$auth\", \"$q\", \"$rootScope\", \"ConfigService\", \"bootstrapped\"];","angular\n .module('nzbhydraApp')\n .controller('HeaderController', HeaderController);\n\nfunction HeaderController($scope, $state, $http, growl, HydraAuthService, ConfigService, bootstrapped) {\n\n $scope.showLoginout = false;\n\n if (ConfigService.getSafe().authType == \"none\") {\n $scope.showAdmin = true;\n $scope.showStats = true;\n $scope.showLoginout = false;\n } else {\n if (HydraAuthService.isLoggedIn()) {\n var rights = HydraAuthService.getUserRights();\n $scope.showAdmin = rights.maySeeAdmin;\n $scope.showStats = rights.maySeeStats;\n $scope.loginlogoutText = \"Logout\";\n $scope.showLoginout = true;\n } else {\n $scope.showAdmin = !bootstrapped.adminRestricted;\n $scope.showStats = !bootstrapped.statsRestricted;\n $scope.loginlogoutText = \"Login\";\n $scope.showLoginout = bootstrapped.adminRestricted || bootstrapped.statsRestricted || bootstrapped.searchRestricted;\n }\n }\n\n function onLogin(data) {\n $scope.showAdmin = data.maySeeAdmin;\n $scope.showStats = data.maySeeStats;\n $scope.showLoginout = true;\n $scope.loginlogoutText = \"Logout\";\n }\n\n $scope.$on(\"user:loggedIn\", function (event, data) {\n onLogin(data);\n });\n\n function onLogout() {\n $scope.showAdmin = !bootstrapped.adminRestricted;\n $scope.showStats = !bootstrapped.statsRestricted;\n $scope.loginlogoutText = \"Login\";\n $scope.showLoginout = bootstrapped.adminRestricted || bootstrapped.statsRestricted || bootstrapped.searchRestricted;\n }\n\n $scope.$on(\"user:loggedOut\", function (event, data) {\n onLogout();\n });\n\n $scope.loginout = function () {\n if (HydraAuthService.isLoggedIn()) {\n HydraAuthService.logout();\n\n if (ConfigService.getSafe().authType == \"basic\") {\n growl.info(\"Logged out. Close your browser to make sure session is closed.\");\n }\n else if (ConfigService.getSafe().authType == \"form\") {\n growl.info(\"Logged out\");\n }\n onLogout();\n $state.go(\"root.search\");\n } else {\n if (ConfigService.getSafe().authType == \"basic\") {\n var params = {};\n if (HydraAuthService.getUserName()) {\n params = {\n old_username: HydraAuthService.getUserName()\n }\n } \n $http.get(\"internalapi/askforpassword\", {params: params}).then(function () {\n growl.info(\"Login successful!\");\n //onLogin();\n $state.go(\"root.search\");\n })\n } else if (ConfigService.getSafe().authType == \"form\") {\n $state.go(\"root.login\");\n } else {\n growl.info(\"You shouldn't need to login but here you go!\");\n }\n }\n }\n}\nHeaderController.$inject = [\"$scope\", \"$state\", \"$http\", \"growl\", \"HydraAuthService\", \"ConfigService\", \"bootstrapped\"];\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) {\n return s.split(\"\").reduce(function (a, b) {\n a = ((a << 5) - a) + b.charCodeAt(0);\n return a & a\n }, 0);\n};\n\nangular\n .module('nzbhydraApp').run([\"formlyConfig\", \"formlyValidationMessages\", function (formlyConfig, formlyValidationMessages) {\n formlyValidationMessages.addStringMessage('required', 'This field is required');\n formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'fc.$touched || form.$submitted';\n\n}]);\n\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n formlyConfigProvider.extras.removeChromeAutoComplete = true;\n formlyConfigProvider.extras.explicitAsync = true;\n formlyConfigProvider.disableWarnings = window.onProd;\n\n\n formlyConfigProvider.setWrapper({\n name: 'settingWrapper',\n templateUrl: 'setting-wrapper.html'\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'fieldset',\n template: [\n '
',\n '{{options.templateOptions.label}}',\n '',\n '
'\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'help',\n template: [\n '
',\n '
',\n '
{{ line }}
',\n '
',\n '
'\n ].join(' ')\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'logicalGroup',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalInput',\n extends: 'input',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'timeOfDay',\n extends: 'horizontalInput',\n controller: ['$scope', function ($scope) {\n $scope.model[$scope.options.key] = moment.utc($scope.model[$scope.options.key]).toDate();\n }]\n });\n\n formlyConfigProvider.setType({\n name: 'percentInput',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'apiKeyInput',\n template: [\n '
',\n '',\n '',\n '',\n '
'\n ].join(' '),\n controller: function ($scope) {\n $scope.generate = function () {\n $scope.model[$scope.options.key] = (Math.random() * 1e32).toString(36);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'testConnection',\n templateUrl: 'button-test-connection.html'\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalTestConnection',\n extends: 'testConnection',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'checkCaps',\n templateUrl: 'button-check-caps.html',\n controller: function ($scope, ConfigBoxService) {\n $scope.message = \"\";\n $scope.uniqueId = hashCode($scope.model.name) + hashCode($scope.model.host);\n\n var testButton = \"#button-check-caps-\" + $scope.uniqueId;\n var testMessage = \"#message-check-caps-\" + $scope.uniqueId;\n\n function showSuccess() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).addClass(\"btn-success\");\n }\n\n function showError() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-danger\");\n }\n\n $scope.checkCaps = function () {\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\n\n var url = \"internalapi/test_caps\";\n var params = {indexer: $scope.model.name, apikey: $scope.model.apikey, host: $scope.model.host};\n ConfigBoxService.checkCaps(url, params, $scope.model).then(function (data, model) {\n angular.element(testMessage).text(\"Supports: \" + data.supportedIds + \",\" ? data.supportedIds && data.supportedTypes : \"\" + data.supportedTypes);\n showSuccess();\n }, function (message) {\n angular.element(testMessage).text(message);\n showError();\n }).finally(function () {\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\n });\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalCheckCaps',\n extends: 'checkCaps',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalApiKeyInput',\n extends: 'apiKeyInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalPercentInput',\n extends: 'percentInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'switch',\n template: \n '
'\n });\n\n\n formlyConfigProvider.setType({\n name: 'duoSetting',\n extends: 'input',\n defaultOptions: {\n className: 'col-md-9',\n templateOptions: {\n type: 'number',\n noRow: true,\n label: ''\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSwitch',\n extends: 'switch',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSelect',\n extends: 'select',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalMultiselect',\n defaultOptions: {\n templateOptions: {\n optionsAttr: 'bs-options',\n ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',\n valueProp: 'id',\n labelProp: 'label',\n getPlaceholder: function() {return \"\";}\n }\n },\n templateUrl: 'ui-select-multiple.html',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'label',\n template: ''\n });\n\n formlyConfigProvider.setType({\n name: 'duolabel',\n extends: 'label',\n defaultOptions: {\n className: 'col-md-2',\n templateOptions: {\n label: '-'\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'repeatSection',\n templateUrl: 'repeatSection.html',\n controller: function ($scope) {\n $scope.formOptions = {formState: $scope.formState};\n $scope.addNew = addNew;\n $scope.remove = remove;\n $scope.copyFields = copyFields;\n\n function copyFields(fields) {\n fields = angular.copy(fields);\n $scope.repeatfields = fields;\n return fields;\n }\n\n $scope.clear = function (field) {\n return _.mapObject(field, function (key, val) {\n if (typeof val === 'object') {\n return $scope.clear(val);\n }\n return undefined;\n\n });\n };\n\n\n function addNew() {\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\n var repeatsection = $scope.model[$scope.options.key];\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\n repeatsection.push(newsection);\n }\n\n function remove($index) {\n $scope.model[$scope.options.key].splice($index, 1);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'arrayConfig',\n templateUrl: 'arrayConfig.html',\n controller: function ($scope, $uibModal) {\n $scope.formOptions = {formState: $scope.formState};\n $scope._showBox = _showBox;\n $scope.showBox = showBox;\n $scope.isInitial = false;\n\n $scope.presets = $scope.options.data.presets;\n\n function _showBox(model, parentModel, isInitial, callback) {\n var modalInstance = $uibModal.open({\n templateUrl: 'configBox.html',\n controller: 'ConfigBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n return model;\n },\n fields: function () {\n return $scope.options.data.fieldsFunction(model, parentModel, isInitial, angular.injector());\n },\n isInitial: function () {\n return isInitial\n },\n parentModel: function () {\n return parentModel;\n },\n data: function () {\n return $scope.options.data;\n }\n }\n });\n\n\n modalInstance.result.then(function () {\n $scope.form.$setDirty(true);\n if (angular.isDefined(callback)) {\n callback(true);\n }\n }, function () {\n if (angular.isDefined(callback)) {\n callback(false);\n }\n });\n }\n\n function showBox(model, parentModel) {\n $scope._showBox(model, parentModel, false)\n }\n\n $scope.addEntry = function (entriesCollection, preset) {\n var model = angular.copy($scope.options.data.defaultModel);\n if (angular.isDefined(preset)) {\n _.extend(model, preset);\n }\n\n $scope.isInitial = true;\n\n $scope._showBox(model, entriesCollection, true, function (isSubmitted) {\n if (isSubmitted) {\n entriesCollection.push(model);\n }\n });\n };\n\n }\n\n });\n\n }]);\n\n\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) {\n\n $scope.model = model;\n $scope.fields = fields;\n $scope.isInitial = isInitial;\n $scope.allowDelete = data.allowDeleteFunction(model);\n $scope.spinnerActive = false;\n $scope.needsConnectionTest = false;\n \n $scope.obSubmit = function () {\n console.log($scope);\n if ($scope.form.$valid) {\n \n var a = data.checkBeforeClose($scope, model).then(function() {\n $uibModalInstance.close($scope);\n });\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n angular.forEach($scope.form.$error, function (error) {\n angular.forEach(error, function (field) {\n field.$setTouched();\n });\n });\n }\n };\n\n $scope.reset = function () {\n $scope.reset();\n };\n\n $scope.deleteEntry = function () {\n parentModel.splice(parentModel.indexOf(model), 1);\n $uibModalInstance.close($scope);\n };\n\n $scope.reset = function () {\n if (angular.isDefined(data.resetFunction)) {\n data.resetFunction($scope);\n }\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\n if (reason == \"backdrop click\") {\n $scope.reset($scope);\n }\n });\n}]);\n\nangular\n .module('nzbhydraApp')\n .factory('ConfigBoxService', ConfigBoxService);\n\nfunction ConfigBoxService($http, $q) {\n\n return {\n checkConnection: checkConnection,\n checkCaps: checkCaps\n };\n\n function checkConnection(url, settings) {\n var deferred = $q.defer();\n\n $http.post(url, settings).success(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \n if (result.result) {\n deferred.resolve();\n } else {\n deferred.reject({checked: true, message: result.message});\n }\n }).error(function (result) {\n deferred.reject({checked: false, message: result.message});\n });\n\n return deferred.promise;\n }\n\n function checkCaps(url, params, model) {\n var deferred = $q.defer();\n\n $http.post(url, params).success(function (data) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \n if (data.success) {\n model.search_ids = data.supportedIds;\n model.searchTypes = data.supportedTypes;\n if (data.supportsAllCategories) { //Don't display all the categories, will be replaced with placeholder \"All categories\"\n model.categories = [];\n } else {\n model.categories = data.supportedCategories;\n }\n model.animeCategory = data.animeCategory;\n model.audiobookCategory = data.audiobookCategory;\n model.comicCategory = data.comicCategory;\n model.ebookCategory = data.ebookCategory;\n model.magazineCategory = data.magazineCategory;\n model.backend = data.backend;\n deferred.resolve({supportedIds: data.supportedIds, supportedTypes: data.supportedTypes}, model);\n } else {\n deferred.reject(data.message);\n }\n }).error(function () {\n deferred.reject(\"Unknown error\");\n });\n\n return deferred.promise;\n }\n\n}\nConfigBoxService.$inject = [\"$http\", \"$q\"];\n\n\n\n\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\n .module('nzbhydraApp')\n .factory('DownloaderCategoriesService', DownloaderCategoriesService);\n\nfunction DownloaderCategoriesService($http, $q, $uibModal) {\n\n var categories = {};\n var selectedCategory = {};\n\n var service = {\n get: getCategories,\n invalidate: invalidate,\n select: select,\n openCategorySelection: openCategorySelection\n };\n\n var deferred;\n\n return service;\n\n\n function getCategories(downloader) {\n\n function loadAll() {\n if (angular.isDefined(categories) && angular.isDefined(categories.downloader)) {\n var deferred = $q.defer();\n deferred.resolve(categories.downloader);\n return deferred.promise;\n }\n \n return $http.get('internalapi/getcategories', {params: {downloader: downloader.name}})\n .then(function (categoriesResponse) {\n \n console.log(\"Updating downloader categories cache\");\n var categories = {downloader: categoriesResponse.data.categories};\n return categoriesResponse.data.categories;\n\n }, function (error) {\n throw error;\n });\n }\n\n return loadAll().then(function (categories) {\n return categories;\n }, function (error) {\n throw error;\n });\n }\n\n\n function openCategorySelection(downloader) {\n $uibModal.open({\n templateUrl: 'static/html/directives/addable-nzb-modal.html',\n controller: 'DownloaderCategorySelectionController',\n size: \"sm\",\n resolve: {\n categories: function () {\n return getCategories(downloader)\n }\n }\n });\n deferred = $q.defer();\n return deferred.promise;\n }\n\n function select(category) {\n selectedCategory = category;\n console.log(\"Selected category \" + category);\n deferred.resolve(category);\n }\n\n function invalidate() {\n console.log(\"Invalidating categories\");\n categories = undefined;\n }\n}\nDownloaderCategoriesService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\n\nangular\n .module('nzbhydraApp').controller('DownloaderCategorySelectionController', [\"$scope\", \"$uibModalInstance\", \"DownloaderCategoriesService\", \"categories\", function ($scope, $uibModalInstance, DownloaderCategoriesService, categories) {\n console.log(categories);\n $scope.categories = categories;\n $scope.select = function (category) {\n DownloaderCategoriesService.select(category);\n $uibModalInstance.close($scope);\n }\n}]);","angular\n .module('nzbhydraApp')\n .controller('DownloadHistoryController', DownloadHistoryController);\n\n\nfunction DownloadHistoryController($scope, StatsService, downloads) {\n $scope.type = \"All\";\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n\n $scope.changeType = function (type) {\n $scope.type = type;\n getDownloadsPage($scope.pagination.current);\n };\n\n\n $scope.pageChanged = function (newPage) {\n getDownloadsPage(newPage);\n };\n\n function getDownloadsPage(pageNumber) {\n StatsService.getDownloadHistory(pageNumber, $scope.limit, $scope.type).then(function(downloads) {\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n });\n \n }\n\n\n}\nDownloadHistoryController.$inject = [\"$scope\", \"StatsService\", \"downloads\"];\n","angular\n .module('nzbhydraApp')\n .factory('ConfigService', ConfigService);\n\nfunction ConfigService($http, $q, $cacheFactory) {\n\n var cache = $cacheFactory(\"nzbhydra\");\n\n return {\n set: set,\n get: get,\n getSafe: getSafe,\n invalidateSafe: invalidateSafe,\n maySeeAdminArea: maySeeAdminArea\n };\n\n\n function set(newConfig) {\n $http.put('internalapi/setsettings', newConfig)\n .then(function (successresponse) {\n console.log(\"Settings saved. Updating cache\");\n cache.put(\"config\", newConfig);\n invalidateSafe();\n }, function (errorresponse) {\n console.log(\"Error saving settings: \" + errorresponse);\n });\n }\n\n function get() {\n var config = cache.get(\"config\");\n if (angular.isUndefined(config)) {\n config = $http.get('internalapi/getconfig').then(function (data) {\n return data.data;\n });\n cache.put(\"config\", config);\n }\n\n return config;\n }\n\n function getSafe() {\n var safeconfig = cache.get(\"safeconfig\");\n if (angular.isDefined(safeconfig)) {\n return safeconfig;\n }\n \n return $http.get('internalapi/getsafeconfig').then(function (data) {\n cache.put(\"safeconfig\", data.data);\n return data.data;\n });\n\n\n }\n\n function invalidateSafe() {\n cache.remove(\"safeconfig\");\n }\n\n function maySeeAdminArea() {\n function loadAll() {\n var maySeeAdminArea = cache.get(\"maySeeAdminArea\");\n if (!angular.isUndefined(maySeeAdminArea)) {\n var deferred = $q.defer();\n deferred.resolve(maySeeAdminArea);\n return deferred.promise;\n }\n\n return $http.get('internalapi/mayseeadminarea')\n .then(function (configResponse) {\n var config = configResponse.data;\n cache.put(\"maySeeAdminArea\", config);\n return configResponse.data;\n });\n }\n\n return loadAll().then(function (maySeeAdminArea) {\n return maySeeAdminArea;\n });\n }\n}\nConfigService.$inject = [\"$http\", \"$q\", \"$cacheFactory\"];","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: '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 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: '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: '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: '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: getDownloaderPresets(),\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 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 },\r\n addNewText: 'Add new indexer',\r\n entryTemplateUrl: 'indexerEntry.html',\r\n presets: getIndexerPresets(),\r\n fieldsFunction: getIndexerBoxFields,\r\n allowDeleteFunction: function (model) {\r\n return model.type == 'newznab' || model.type == 'jackett';\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 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() {\r\n return [\r\n [\r\n {\r\n name: \"6box\",\r\n host: \"https://6box.me\"\r\n },\r\n {\r\n name: \"6box sptweb\",\r\n host: \"https://6box.me/spotweb\"\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://drunkenslug.com\"\r\n },\r\n {\r\n name: \"miatrix\",\r\n host: \"https://www.miatrix.com\"\r\n },\r\n {\r\n name: \"NZB Finder\",\r\n host: \"https://nzbfinder.ws\"\r\n },\r\n {\r\n name: \"NZBs.org\",\r\n host: \"https://nzbs.org\"\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: \"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: \"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: \"SimplyNZBs\",\r\n host: \"https://simplynzbs.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\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 fieldset.push(\r\n {\r\n key: 'hitLimitResetTime',\r\n type: 'horizontalInput',\r\n hideExpression: '!model.hitLimit',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'API 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\r\n\r\n if (model.type != \"womble\") {\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 }\r\n if (model.type != \"womble\" && 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 != \"womble\" && 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 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.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\"];","angular\n .module('nzbhydraApp')\n .factory('ConfigModel', function () {\n return {};\n });\n\nangular\n .module('nzbhydraApp')\n .factory('ConfigWatcher', function () {\n var $scope;\n\n return {\n watch: watch\n };\n\n function watch(scope) {\n $scope = scope;\n $scope.$watchGroup([\"config.main.host\"], function () {\n }, true);\n }\n });\n\n\nangular\n .module('nzbhydraApp')\n .controller('ConfigController', ConfigController);\n\nfunction ConfigController($scope, $http, ConfigService, config, DownloaderCategoriesService, ConfigFields, ConfigModel, ModalService, RestartService, $state, growl, $rootScope) {\n $scope.config = config;\n $scope.submit = submit;\n $scope.activeTab = undefined;\n\n $scope.restartRequired = false;\n $scope.ignoreSaveNeeded = false;\n\n ConfigFields.setRestartWatcher(function () {\n $scope.restartRequired = true;\n });\n \n\n function submit() {\n if ($scope.form.$valid) {\n \n ConfigService.set($scope.config);\n ConfigService.invalidateSafe();\n $scope.form.$setPristine();\n DownloaderCategoriesService.invalidate();\n if ($scope.restartRequired) {\n ModalService.open(\"Restart required\", \"The changes you have made may require a restart to be effective.
Do you want to restart now?\", {\n yes: {\n onYes: function () {\n RestartService.restart();\n }\n },\n no: {\n onNo: function () {\n $scope.restartRequired = false;\n }\n }\n });\n }\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n \n //Ridiculously hacky way to make the error messages appear\n try {\n if (angular.isDefined(form.$error.required)) {\n _.each(form.$error.required, function (item) {\n if (angular.isDefined(item.$error.required)) {\n _.each(item.$error.required, function (item2) {\n item2.$setTouched();\n });\n } \n });\n }\n angular.forEach($scope.form.$error.required, function (field) {\n field.$setTouched();\n });\n } catch(err) {\n //\n }\n \n }\n }\n\n ConfigModel = config;\n\n $scope.fields = ConfigFields.getFields($scope.config);\n \n $scope.allTabs = [\n {\n active: false,\n state: 'root.config',\n name: 'Main',\n model: ConfigModel.main,\n fields: $scope.fields.main\n },\n {\n active: false,\n state: 'root.config.auth',\n name: 'Authorization',\n model: ConfigModel.auth,\n fields: $scope.fields.auth\n },\n {\n active: false,\n state: 'root.config.searching',\n name: 'Searching',\n model: ConfigModel.searching,\n fields: $scope.fields.searching\n },\n {\n active: false,\n state: 'root.config.categories',\n name: 'Categories',\n model: ConfigModel.categories,\n fields: $scope.fields.categories\n },\n {\n active: false,\n state: 'root.config.downloader',\n name: 'Downloaders',\n model: ConfigModel.downloaders,\n fields: $scope.fields.downloaders\n },\n {\n active: false,\n state: 'root.config.indexers',\n name: 'Indexers',\n model: ConfigModel.indexers,\n fields: $scope.fields.indexers\n }\n ];\n\n for (var i = 0; i < $scope.allTabs.length; i++) {\n if ($state.is($scope.allTabs[i].state)) {\n $scope.allTabs[i].active = true;\n $scope.activeTab = $scope.allTabs[i];\n }\n }\n\n $scope.isSavingNeeded = function () {\n return $scope.form.$dirty && $scope.form.$valid && !$scope.ignoreSaveNeeded;\n };\n\n $scope.goToConfigState = function (index) {\n $state.go($scope.allTabs[index].state);\n $scope.activeTab = $scope.allTabs[index]; \n };\n \n $scope.help = function() {\n $http.get(\"internalapi/gethelp\", {params: {id: $scope.activeTab.name}}).then(function(result) {\n var html = '' + result.data + \"\";\n ModalService.open($scope.activeTab.name + \" - Help\", html, {}, \"lg\");\n },\n function() {\n growl.error(\"Error while loading help\")\n })\n };\n\n $scope.$on('$stateChangeStart',\n function (event, toState, toParams, fromState, fromParams) {\n if ($scope.isSavingNeeded()) {\n event.preventDefault();\n ModalService.open(\"Unsaved changed\", \"Do you want to save before leaving?\", {\n yes: {\n onYes: function() {\n $scope.submit();\n $state.go(toState);\n },\n text: \"Yes\"\n },\n no: {\n onNo: function () {\n $scope.ignoreSaveNeeded = true;\n $scope.ctrl.options.resetModel();\n $state.go(toState);\n },\n text: \"No\"\n },\n cancel: {\n onCancel: function () {\n event.preventDefault();\n },\n text: \"Cancel\"\n }\n });\n } \n })\n}\nConfigController.$inject = [\"$scope\", \"$http\", \"ConfigService\", \"config\", \"DownloaderCategoriesService\", \"ConfigFields\", \"ConfigModel\", \"ModalService\", \"RestartService\", \"$state\", \"growl\", \"$rootScope\"];\n\n\n","angular\n .module('nzbhydraApp')\n .factory('CategoriesService', CategoriesService);\n\nfunction CategoriesService(ConfigService) {\n\n return {\n getByName: getByName,\n getAll: getAll,\n getDefault: getDefault\n };\n\n\n function getByName(name) {\n for (var category in ConfigService.getSafe().categories) {\n category = ConfigService.getSafe().categories[category];\n if (category.name == name || category.pretty == name) {\n return category;\n }\n }\n }\n \n function getAll() {\n return ConfigService.getSafe().categories;\n }\n \n function getDefault() {\n return getAll()[1];\n }\n\n}\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/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-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;ACxpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACtFA;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;ACnBA;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;AC/EA;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;AACA;ACrGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7QA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACxIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1QA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACrEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACvcA;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;ACpCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzuDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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', 'satellizer', 'LocalStorageModule', 'angular.filter', 'ngFileUpload']);\n\nangular.module('nzbhydraApp').config([\"$stateProvider\", \"$urlRouterProvider\", \"$locationProvider\", \"blockUIConfig\", \"$urlMatcherFactoryProvider\", \"$authProvider\", \"localStorageServiceProvider\", \"bootstrapped\", function ($stateProvider, $urlRouterProvider, $locationProvider, blockUIConfig, $urlMatcherFactoryProvider, $authProvider, localStorageServiceProvider, bootstrapped) {\n\n blockUIConfig.autoBlock = false;\n $urlMatcherFactoryProvider.strictMode(false);\n\n $urlRouterProvider.otherwise(\"/\");\n\n\n $stateProvider\n .state('root', {\n url: '',\n abstract: true,\n resolve: {\n //loginRequired: loginRequired\n },\n views: {\n 'header': {\n templateUrl: 'static/html/states/header.html',\n controller: 'HeaderController'\n },\n 'footer': {\n templateUrl: 'footer.html'\n }\n }\n })\n .state(\"root.config\", {\n url: \"/config\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n controllerAs: 'ctrl',\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config\"\n }]\n }\n }\n }\n })\n .state(\"root.config.auth\", {\n url: \"/auth\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Auth)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.searching\", {\n url: \"/searching\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Searching)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.categories\", {\n url: \"/categories\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Categories)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.downloader\", {\n url: \"/downloader\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Downloader)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.indexers\", {\n url: \"/indexers\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.system\", {\n url: \"/system\",\n templateUrl: \"static/html/states/config.html\",\n views: {\n 'container@': {\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System\"\n }]\n }\n }\n }\n })\n .state(\"root.config.log\", {\n url: \"/log\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Log)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats\", {\n url: \"/stats\",\n abstract: true,\n views: {\n 'container@': {\n templateUrl: \"static/html/states/stats.html\",\n controller: [\"$scope\", \"$state\", function($scope, $state) {\n $scope.$state = $state;\n }],\n resolve: {\n loginRequired: loginRequiredStats,\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n \n }\n } \n })\n .state(\"root.stats.main\", {\n url: \"/stats\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/main-stats.html\",\n controller: \"StatsController\",\n resolve: {\n loginRequired: loginRequiredStats,\n stats: ['loginRequired', 'StatsService', function (loginRequired, StatsService) {\n return StatsService.get();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.indexers\", {\n url: \"/indexers\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/indexer-statuses.html\",\n controller: IndexerStatusesController,\n resolve: {\n loginRequired: loginRequiredStats,\n statuses: [\"$http\", function($http) {\n return $http.get(\"internalapi/getindexerstatuses\").success(function (response) {\n return response.indexerStatuses;\n });\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.searches\", {\n url: \"/searches\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/search-history.html\",\n controller: SearchHistoryController,\n resolve: {\n loginRequired: loginRequiredStats,\n history: [\"StatsService\", function(StatsService) {\n return StatsService.getSearchHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Searches)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.downloads\", {\n url: \"/downloads\",\n views: {\n 'stats@root.stats': {\n templateUrl: 'static/html/states/download-history.html',\n controller: DownloadHistoryController,\n resolve: {\n loginRequired: loginRequiredStats,\n downloads: [\"StatsService\", function (StatsService) {\n return StatsService.getDownloadHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Downloads)\"\n }]\n }\n }\n }\n })\n .state(\"root.system\", {\n url: \"/system\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n askAdmin: ['loginRequired', '$http', function (loginRequired, $http) {\n return $http.get(\"internalapi/askadmin\");\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System\"\n }]\n }\n }\n }\n })\n .state(\"root.system.updates\", {\n url: \"/updates\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Updates)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.log\", {\n url: \"/log\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Log)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.backup\", {\n url: \"/backup\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Backup)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.about\", {\n url: \"/about\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (About)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.bugreport\", {\n url: \"/bugreport\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: loginRequiredAdmin,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Bug report)\"\n }]\n }\n }\n }\n })\n .state(\"root.search\", {\n url: \"/?category&query&imdbid&tvdbid&title&season&episode&minsize&maxsize&minage&maxage&offsets&rid&mode&tmdbid&indexers\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/search.html\",\n controller: \"SearchController\",\n resolve: {\n loginRequired: loginRequiredSearch,\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Search\";\n }]\n }\n }\n }\n })\n .state(\"root.search.results\", {\n views: {\n 'results@root.search': {\n templateUrl: \"static/html/states/search-results.html\",\n controller: \"SearchResultsController\",\n controllerAs: \"srController\",\n options: {\n inherit: true\n },\n resolve: {\n loginRequired: loginRequiredSearch,\n $title: [\"$stateParams\", function ($stateParams) {\n console.log($stateParams);\n var title = \"Search results\";\n var details; \n if ($stateParams.title) {\n details = $stateParams.title;\n } else if ($stateParams.query) {\n details = $stateParams.query;\n }\n if (details) {\n title += \" (\" + details + \")\";\n }\n return title;\n }]\n }\n }\n }\n })\n .state(\"root.login\", {\n url: \"/login\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/login.html\",\n controller: \"LoginController\",\n resolve: {\n loginRequired: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Login\"\n }]\n }\n }\n }\n })\n ;\n\n\n $locationProvider.html5Mode(true);\n\n $authProvider.httpInterceptor = function () {\n return true;\n };\n $authProvider.withCredentials = true;\n $authProvider.tokenRoot = null;\n $authProvider.baseUrl = bootstrapped.baseUrl;\n $authProvider.loginUrl = '/auth/login';\n $authProvider.signupUrl = '/auth/signup';\n $authProvider.unlinkUrl = '/unlink/';\n $authProvider.tokenName = 'token';\n $authProvider.tokenPrefix = 'satellizer';\n $authProvider.authHeader = 'TokenAuthorization';\n $authProvider.authToken = 'Bearer';\n $authProvider.storageType = 'localStorage';\n\n\n //Because I don't know for what state the login is required / asked I have a function for each \n\n function loginRequiredSearch($q, $timeout, $auth, $state, bootstrapped) {\n \n var deferred = $q.defer();\n\n if (bootstrapped.authType != \"form\" || $auth.isAuthenticated() || !bootstrapped.searchRestricted) {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n loginRequiredSearch.$inject = [\"$q\", \"$timeout\", \"$auth\", \"$state\", \"bootstrapped\"];\n\n function loginRequiredStats($q, $timeout, $auth, $state, bootstrapped) {\n var deferred = $q.defer();\n\n if (bootstrapped.authType != \"form\" || $auth.isAuthenticated() || !bootstrapped.statsRestricted) {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n loginRequiredStats.$inject = [\"$q\", \"$timeout\", \"$auth\", \"$state\", \"bootstrapped\"];\n\n function loginRequiredAdmin($q, $timeout, $auth, $state, bootstrapped) {\n var deferred = $q.defer();\n\n if (bootstrapped.authType != \"form\" || $auth.isAuthenticated() || !bootstrapped.adminRestricted) {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n loginRequiredAdmin.$inject = [\"$q\", \"$timeout\", \"$auth\", \"$state\", \"bootstrapped\"];\n\n localStorageServiceProvider\n .setPrefix('nzbhydra');\n localStorageServiceProvider\n .setNotify(true, false);\n}]);\n\n\nnzbhydraapp.config([\"paginationTemplateProvider\", function (paginationTemplateProvider) {\n paginationTemplateProvider.setPath('static/html/dirPagination.tpl.html');\n}]);\n\nnzbhydraapp.config(['cfpLoadingBarProvider', function (cfpLoadingBarProvider) {\n cfpLoadingBarProvider.latencyThreshold = 100;\n}]);\n\nnzbhydraapp.config(['growlProvider', function (growlProvider) {\n growlProvider.globalTimeToLive(5000);\n growlProvider.globalPosition('bottom-right');\n}]);\n\nnzbhydraapp.directive('ngEnter', function () {\n return function (scope, element, attr) {\n element.bind(\"keydown keypress\", function (event) {\n if (event.which === 13) {\n scope.$apply(function () {\n scope.$evalAsync(attr.ngEnter);\n });\n\n event.preventDefault();\n }\n });\n };\n});\n\nnzbhydraapp.filter('nzblink', function () {\n return function (resultItem) {\n var uri = new URI(\"internalapi/getnzb\");\n uri.addQuery(\"searchResultId\", resultItem.searchResultId);\n return uri.toString();\n }\n});\n\nnzbhydraapp.factory('focus', [\"$rootScope\", \"$timeout\", function ($rootScope, $timeout) {\n return function (name) {\n $timeout(function () {\n $rootScope.$broadcast('focusOn', name);\n });\n }\n}]);\n\nnzbhydraapp.run([\"$rootScope\", function ($rootScope) {\n $rootScope.$on('$stateChangeSuccess',\n function (event, toState, toParams, fromState, fromParams) {\n try {\n $rootScope.title = toState.views[Object.keys(toState.views)[0]].resolve.$title[1](toParams);\n } catch(e) {\n \n }\n \n });\n}]);\n\n\nnzbhydraapp.filter('unsafe', [\"$sce\", function ($sce) {\n return $sce.trustAsHtml;\n}]);\n\nnzbhydraapp.filter('dereferer', [\"ConfigService\", function (ConfigService) {\n return function(url) {\n if (ConfigService.getSafe().dereferer) {\n return ConfigService.getSafe().dereferer.replace(\"$s\", escape(url));\n }\n return url;\n }\n}]);\n\nnzbhydraapp.config([\"$provide\", function ($provide) {\n $provide.decorator(\"$exceptionHandler\", ['$delegate', '$injector', function ($delegate, $injector) {\n return function (exception, cause) {\n $delegate(exception, cause);\n try {\n console.log(exception);\n var stack = exception.stack.split('\\n').map(function (line) {\n return line.trim();\n });\n stack = stack.join(\"\\n\");\n //$injector.get(\"$http\").put(\"internalapi/logerror\", {error: stack, cause: angular.isDefined(cause) ? cause.toString() : \"No known cause\"});\n\n\n } catch (e) {\n console.error(\"Unable to log JS exception to server\", e);\n }\n };\n }]);\n}]);\n\n_.mixin({\n isNullOrEmpty: function (string) {\n return (_.isUndefined(string) || _.isNull(string) || (_.isString(string) && string.length === 0))\n }\n});\n\nnzbhydraapp.factory('sessionInjector', [\"$injector\", function ($injector) {\n var sessionInjector = {\n response: function (response) {\n if (response.headers(\"Hydra-MaySeeAdmin\") != null) {\n $injector.get(\"HydraAuthService\").setLoggedInByBasic(response.headers(\"Hydra-MaySeeStats\") == \"True\", response.headers(\"Hydra-MaySeeAdmin\") == \"True\", response.headers(\"Hydra-Username\"))\n }\n \n return response;\n }\n };\n return sessionInjector;\n}]);\n\nnzbhydraapp.config(['$httpProvider', function ($httpProvider) {\n $httpProvider.interceptors.push('sessionInjector');\n}]);\n\nnzbhydraapp.directive('autoFocus', [\"$timeout\", function ($timeout) {\n return {\n restrict: 'AC',\n link: function (_scope, _element) {\n $timeout(function () {\n _element[0].focus();\n }, 0);\n }\n };\n}]);","angular\n .module('nzbhydraApp')\n .directive('hydraupdates', hydraupdates);\n\nfunction hydraupdates() {\n controller.$inject = [\"$scope\", \"UpdateService\", \"$sce\"];\n return {\n templateUrl: 'static/html/directives/updates.html',\n controller: controller\n };\n\n function controller($scope, UpdateService, $sce) {\n\n $scope.loadingPromise = UpdateService.getVersions().then(function (data) {\n $scope.currentVersion = data.data.currentVersion;\n $scope.repVersion = data.data.repVersion;\n $scope.updateAvailable = data.data.updateAvailable;\n $scope.changelog = data.data.changelog;\n });\n \n UpdateService.getVersionHistory().then(function(data) {\n $scope.versionHistory = $sce.trustAsHtml(data.data.versionHistory);\n });\n\n $scope.update = function () {\n UpdateService.update();\n };\n\n $scope.showChangelog = function () {\n UpdateService.showChanges($scope.changelog);\n };\n \n \n\n }\n}\n\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\n .module('nzbhydraApp')\n .directive('tabOrChart', tabOrChart);\n\nfunction tabOrChart() {\n return {\n templateUrl: 'static/html/directives/tab-or-chart.html',\n transclude: {\n \"chartSlot\": \"chart\",\n \"tableSlot\": \"table\"\n },\n restrict: 'E',\n replace: true,\n scope: {\n display: \"@\"\n }\n\n };\n\n}\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\n .module('nzbhydraApp')\n .directive('otherColumns', otherColumns);\n\nfunction otherColumns($http, $templateCache, $compile, $window) {\n controller.$inject = [\"$scope\", \"$http\", \"$uibModal\", \"growl\"];\n return {\n scope: {\n result: \"<\"\n },\n multiElement: true,\n\n link: function (scope, element, attrs) {\n $http.get('static/html/directives/search-result-non-title-columns.html', {cache: $templateCache}).success(function (templateContent) {\n element.replaceWith($compile(templateContent)(scope));\n });\n\n },\n controller: controller\n };\n\n function controller($scope, $http, $uibModal, growl) {\n \n $scope.showNfo = showNfo;\n function showNfo(resultItem) {\n if (resultItem.has_nfo == 0) {\n return;\n }\n var uri = new URI(\"internalapi/getnfo\");\n uri.addQuery(\"searchresultid\", resultItem.searchResultId);\n return $http.get(uri.toString()).then(function (response) {\n if (response.data.has_nfo) {\n $scope.openModal(\"lg\", response.data.nfo)\n } else {\n if (!angular.isUndefined(resultItem.message)) {\n growl.error(resultItem.message);\n } else {\n growl.info(\"No NFO available\");\n }\n }\n });\n }\n\n $scope.openModal = openModal;\n\n function openModal(size, nfo) {\n var modalInstance = $uibModal.open({\n template: '
',\n controller: 'NfoModalInstanceCtrl',\n size: size,\n resolve: {\n nfo: function () {\n return nfo;\n }\n }\n });\n\n modalInstance.result.then();\n }\n \n $scope.downloadNzb = downloadNzb;\n \n function downloadNzb(resultItem) {\n //href = \"{{ result.link }}\"\n $window.location.href = resultItem.link;\n }\n }\n}\notherColumns.$inject = [\"$http\", \"$templateCache\", \"$compile\", \"$window\"];\n\nangular\n .module('nzbhydraApp')\n .controller('NfoModalInstanceCtrl', NfoModalInstanceCtrl);\n\nfunction NfoModalInstanceCtrl($scope, $modalInstance, nfo) {\n\n $scope.nfo = nfo;\n\n $scope.ok = function () {\n $modalInstance.close($scope.selected.item);\n };\n\n $scope.cancel = function () {\n $modalInstance.dismiss();\n };\n}\nNfoModalInstanceCtrl.$inject = [\"$scope\", \"$modalInstance\", \"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\n .module('nzbhydraApp')\n .directive('hydralog', hydralog);\n\nfunction hydralog() {\n controller.$inject = [\"$scope\", \"$http\", \"$sce\"];\n return {\n template: '
',\n controller: controller\n };\n\n function controller($scope, $http, $sce) {\n $scope.logPromise = $http.get(\"internalapi/getlogs\").success(function (data) {\n $scope.log = $sce.trustAsHtml(data.log);\n });\n\n }\n}\n\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\n .module('nzbhydraApp')\n .directive('indexerInput', indexerInput);\n\nfunction indexerInput() {\n controller.$inject = [\"$scope\"];\n return {\n templateUrl: 'static/html/directives/indexer-input.html',\n scope: {\n indexer: \"=\",\n model: \"=\",\n onClick: \"=\"\n },\n replace: true,\n controller: controller\n };\n\n function controller($scope) {\n $scope.isFocused = false;\n \n $scope.onFocus = function() {\n $scope.isFocused = true;\n };\n\n $scope.onBlur = function () {\n $scope.isFocused = false; \n };\n \n }\n}\n\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\n .module('nzbhydraApp')\n .directive('downloadNzbsButton', downloadNzbsButton);\n\nfunction downloadNzbsButton() {\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\n return {\n templateUrl: 'static/html/directives/download-nzbs-button.html',\n require: ['^searchResults'],\n scope: {\n searchResults: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService, growl) {\n\n $scope.downloaders = NzbDownloadService.getEnabledDownloaders();\n\n $scope.download = function (downloader) {\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length == 0) {\n growl.info(\"You should select at least one result...\");\n } else {\n\n var values = _.map($scope.searchResults, function (value) {\n return value.searchResultId;\n });\n\n NzbDownloadService.download(downloader, values).then(function (response) {\n if (response.data.success) {\n growl.info(\"Successfully added \" + response.data.added + \" of \" + response.data.of + \" NZBs\");\n } else {\n growl.error(\"Error while adding NZBs\");\n }\n }, function () {\n growl.error(\"Error while adding NZBs\");\n });\n }\n }\n\n\n }\n}\n\n","angular\n .module('nzbhydraApp')\n .directive('connectionTest', connectionTest);\n\nfunction connectionTest() {\n controller.$inject = [\"$scope\"];\n return {\n templateUrl: 'static/html/directives/connection-test.html',\n require: ['^type', '^data'],\n scope: {\n type: \"=\",\n id: \"=\",\n data: \"=\",\n downloader: \"=\"\n },\n controller: controller\n };\n\n function controller($scope) {\n $scope.message = \"\";\n console.log($scope);\n\n var testButton = \"#button-test-connection\";\n var testMessage = \"#message-test-connection\";\n\n function showSuccess() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).addClass(\"btn-success\");\n }\n\n function showError() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-danger\");\n }\n\n $scope.testConnection = function () {\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\n var myInjector = angular.injector([\"ng\"]);\n var $http = myInjector.get(\"$http\");\n var url;\n var params;\n if ($scope.type == \"downloader\") {\n url = \"internalapi/test_downloader\";\n params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};\n if ($scope.downloader == \"sabnzbd\") {\n params.apikey = $scope.data.apikey;\n params.url = $scope.data.url;\n } else {\n params.host = $scope.data.host;\n params.port = $scope.data.port;\n params.ssl = $scope.data.ssl;\n }\n } else if ($scope.data.type == \"newznab\") {\n url = \"internalapi/test_newznab\";\n params = {host: $scope.data.host, apikey: $scope.data.apikey};\n }\n $http.get(url, {params: params}).success(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \n if (result.result) {\n angular.element(testMessage).text(\"\");\n showSuccess();\n } else {\n angular.element(testMessage).text(result.message);\n showError();\n }\n\n }).error(function () {\n angular.element(testMessage).text(result.message);\n showError();\n }).finally(function () {\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\n })\n }\n\n }\n}\n\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\n .module('nzbhydraApp')\n .directive('addableNzbs', addableNzbs);\n\nfunction addableNzbs() {\n controller.$inject = [\"$scope\", \"NzbDownloadService\"];\n return {\n templateUrl: 'static/html/directives/addable-nzbs.html',\n require: ['^searchResultId'],\n scope: {\n searchResultId: \"<\",\n downloadType: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService) {\n $scope.downloaders = _.filter(NzbDownloadService.getEnabledDownloaders(), function(downloader) {\n if ($scope.downloadType != \"nzb\") {\n return downloader.downloadType == $scope.downloadType\n }\n return true;\n });\n }\n}\n","angular\n .module('nzbhydraApp')\n .directive('addableNzb', addableNzb);\n\nfunction addableNzb() {\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\n return {\n templateUrl: 'static/html/directives/addable-nzb.html',\n scope: {\n searchResultId: \"<\",\n downloader: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService, growl) {\n if ($scope.downloader.iconCssClass) {\n $scope.cssClass = \"fa fa-\" + $scope.downloader.iconCssClass.replace(\"fa-\",\"\").replace(\"fa \", \"\"); \n } else {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd\" : \"nzbget\";\n }\n \n $scope.add = function () {\n $scope.cssClass = \"nzb-spinning\";\n NzbDownloadService.download($scope.downloader, [$scope.searchResultId]).then(function (response) {\n if (response.data.success) {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-success\" : \"nzbget-success\";\n } else {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\n growl.error(\"Unable to add NZB. Make sure the downloader is running and properly configured.\");\n }\n }, function () {\n $scope.cssClass = $scope.downloader.type == \"sabnzbd\" ? \"sabnzbd-error\" : \"nzbget-error\";\n growl.error(\"An unexpected error occurred while trying to contact NZB Hydra or add the NZB.\");\n })\n };\n \n \n\n }\n}\n","angular\n .module('nzbhydraApp')\n .factory('UpdateService', UpdateService);\n\nfunction UpdateService($http, growl, blockUI, RestartService) {\n\n var currentVersion;\n var repVersion;\n var updateAvailable;\n var changelog;\n var versionHistory;\n \n return {\n update: update,\n showChanges: showChanges,\n getVersions: getVersions,\n getChangelog: getChangelog,\n getVersionHistory: getVersionHistory\n };\n \n \n \n function getVersions() {\n return $http.get(\"internalapi/get_versions\").then(function (data) {\n currentVersion = data.data.currentVersion;\n repVersion = data.data.repVersion;\n updateAvailable = data.data.updateAvailable;\n return data;\n });\n }\n\n function getChangelog() {\n return $http.get(\"internalapi/get_changelog\", {currentVersion: currentVersion, repVersion: repVersion}).then(function (data) {\n changelog = data.data.changelog;\n return data;\n });\n }\n \n function getVersionHistory() {\n return $http.get(\"internalapi/get_version_history\").then(function (data) {\n versionHistory = data.data.versionHistory;\n return data;\n });\n }\n\n function showChanges(changelog) {\n\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\n var $uibModal = myInjector.get(\"$uibModal\");\n var params = {\n size: \"lg\",\n templateUrl: \"static/html/changelog.html\",\n resolve: {\n changelog: function () {\n return changelog;\n }\n },\n controller: function ($scope, $sce, $uibModalInstance, changelog) {\n //I fucking hate that untrusted HTML shit\n changelog = $sce.trustAsHtml(changelog);\n $scope.changelog = changelog;\n console.log(changelog);\n $scope.ok = function () {\n $uibModalInstance.dismiss();\n };\n }\n };\n\n var modalInstance = $uibModal.open(params);\n\n modalInstance.result.then();\n }\n \n\n function update() {\n blockUI.start(\"Updating. Please stand by...\");\n $http.get(\"internalapi/update\").then(function (data) {\n if (data.data.success) {\n RestartService.restart(\"Update complete.\", 15);\n } else {\n blockUI.reset();\n growl.info(\"An error occurred while updating. Please check the logs.\");\n }\n },\n function () {\n blockUI.reset();\n growl.info(\"An error occurred while updating. Please check the logs.\");\n });\n }\n}\nUpdateService.$inject = [\"$http\", \"growl\", \"blockUI\", \"RestartService\"];\n\n","angular\n .module('nzbhydraApp')\n .controller('UpdateFooterController', UpdateFooterController);\n\nfunction UpdateFooterController($scope, UpdateService, HydraAuthService, bootstrapped) {\n\n $scope.updateAvailable = false;\n $scope.checked = false;\n\n $scope.mayUpdate = HydraAuthService.getUserRights().maySeeAdmin || bootstrapped.maySeeAdmin;\n\n $scope.$on(\"user:loggedIn\", function (event, data) {\n if (data.maySeeAdmin && !$scope.checked) {\n retrieveUpdateInfos();\n }\n });\n\n\n if ($scope.mayUpdate) {\n retrieveUpdateInfos();\n }\n\n function retrieveUpdateInfos() {\n $scope.checked = true;\n UpdateService.getVersions().then(function (data) {\n $scope.currentVersion = data.data.currentVersion;\n $scope.repVersion = data.data.repVersion;\n $scope.updateAvailable = data.data.updateAvailable;\n $scope.changelog = data.data.changelog;\n });\n }\n\n\n $scope.update = function () {\n UpdateService.update();\n };\n\n $scope.showChangelog = function () {\n UpdateService.showChanges($scope.changelog);\n }\n\n}\nUpdateFooterController.$inject = [\"$scope\", \"UpdateService\", \"HydraAuthService\", \"bootstrapped\"];\n","angular\n .module('nzbhydraApp')\n .controller('SystemController', SystemController);\n\nfunction SystemController($scope, $state, $http, growl, RestartService, ModalService, UpdateService, NzbHydraControlService) {\n\n\n $scope.shutdown = function () {\n NzbHydraControlService.shutdown().then(function () {\n growl.info(\"Shutdown initiated. Cya!\");\n },\n function () {\n growl.info(\"Unable to send shutdown command.\");\n })\n };\n\n $scope.restart = function () {\n RestartService.restart();\n };\n\n $scope.deleteLogAndDatabase = function () {\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.\", {\n yes: {\n onYes: function () {\n NzbHydraControlService.deleteLogAndDb();\n RestartService.countdown();\n },\n text: \"Yes, delete log and database\"\n },\n no: {\n onCancel: function () {\n\n },\n text: \"Nah\"\n }\n });\n };\n\n $scope.forceUpdate = function() {\n UpdateService.update()\n };\n \n\n $scope.tabs = [\n {\n active: false,\n state: 'root.system'\n },\n {\n active: false,\n state: 'root.system.updates'\n },\n {\n active: false,\n state: 'root.system.log'\n },\n {\n active: false,\n state: 'root.system.backup'\n },\n {\n active: false,\n state: 'root.system.bugreport'\n },\n {\n active: false,\n state: 'root.system.about'\n }\n ];\n\n\n for (var i = 0; i < $scope.tabs.length; i++) {\n if ($state.is($scope.tabs[i].state)) {\n $scope.tabs[i].active = true;\n }\n }\n\n\n $scope.goToState = function (index) {\n $state.go($scope.tabs[index].state);\n \n };\n\n $scope.downloadDebuggingInfos = function() {\n $http({method: 'GET', url: 'internalapi/getdebugginginfos', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {\n var a = document.createElement('a');\n var blob = new Blob([data], {'type': \"application/octet-stream\"});\n a.href = URL.createObjectURL(blob);\n var filename = \"nzbhydra-debuginfo-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\n a.download = filename;\n \n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }).error(function (data, status, headers, config) {\n console.log(\"Error:\" + status);\n });\n }\n \n}\nSystemController.$inject = [\"$scope\", \"$state\", \"$http\", \"growl\", \"RestartService\", \"ModalService\", \"UpdateService\", \"NzbHydraControlService\"];\n","angular\n .module('nzbhydraApp')\n .factory('StatsService', StatsService);\n\nfunction StatsService($http) {\n\n return {\n get: getStats,\n getSearchHistory: getSearchHistory,\n getDownloadHistory: getDownloadHistory\n };\n\n function getStats(after, before) {\n return $http.get(\"internalapi/getstats\", {params: {after:after, before:before}}).success(function (response) {\n return response.data;\n });\n }\n\n function getSearchHistory(pageNumber, limit, type) {\n if (angular.isUndefined(pageNumber)) {\n pageNumber = 1;\n }\n if (angular.isUndefined(limit)) {\n limit = 100;\n }\n if (angular.isUndefined(type)) {\n type = \"All\";\n }\n return $http.get(\"internalapi/getsearchrequests\", {params: {page: pageNumber, limit: limit, type: type}}).success(function (response) {\n return {\n searchRequests: response.searchRequests,\n totalRequests: response.totalRequests\n }\n });\n }\n \n function getDownloadHistory(pageNumber, limit, type) {\n if (angular.isUndefined(pageNumber)) {\n pageNumber = 1;\n }\n if (angular.isUndefined(limit)) {\n limit = 100;\n }\n if (angular.isUndefined(type)) {\n type = \"All\";\n }\n console.log(1);\n return $http.get(\"internalapi/getnzbdownloads\", {params: {page: pageNumber, limit: limit, type: type}}).success(function (response) {\n console.log(2);\n return {\n nzbDownloads: response.nzbDownloads,\n totalDownloads: response.totalDownloads\n };\n \n });\n }\n\n}\nStatsService.$inject = [\"$http\"];","angular\n .module('nzbhydraApp')\n .controller('StatsController', StatsController);\n\nfunction StatsController($scope, $filter, StatsService, blockUI, stats) {\n\n $scope.dateOptions = {\n dateDisabled: false,\n formatYear: 'yy',\n startingDay: 1\n };\n\n $scope.afterDate = null;\n $scope.beforeDate = null;\n\n $scope.openAfter = function () {\n $scope.after.opened = true;\n };\n\n $scope.openBefore = function () {\n $scope.before.opened = true;\n };\n\n $scope.after = {\n opened: false\n };\n\n $scope.before = {\n opened: false\n };\n\n\n function updateStats() {\n blockUI.start(\"Updating stats...\");\n var after = $scope.afterDate != null ? $scope.afterDate.getTime() / 1000 : null;\n var before = $scope.beforeDate != null ? $scope.beforeDate.getTime() / 1000 : null;\n StatsService.get(after, before).then(function(stats) {\n $scope.setStats(stats);\n });\n\n blockUI.reset();\n }\n\n $scope.$watch('beforeDate', function () {\n updateStats();\n });\n\n $scope.$watch('afterDate', function () {\n updateStats();\n });\n\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\n $scope.format = $scope.formats[0];\n $scope.altInputFormats = ['M!/d!/yyyy'];\n\n $scope.setStats = function (stats) {\n stats = stats.data;\n\n $scope.nzbDownloads = null;\n $scope.avgResponseTimes = stats.avgResponseTimes;\n $scope.avgIndexerSearchResultsShares = stats.avgIndexerSearchResultsShares;\n $scope.avgIndexerAccessSuccesses = stats.avgIndexerAccessSuccesses;\n $scope.indexerDownloadShares = stats.indexerDownloadShares;\n $scope.downloadsPerHourOfDay = stats.timeBasedDownloadStats.perHourOfDay;\n $scope.downloadsPerDayOfWeek = stats.timeBasedDownloadStats.perDayOfWeek;\n $scope.searchesPerHourOfDay = stats.timeBasedSearchStats.perHourOfDay;\n $scope.searchesPerDayOfWeek = stats.timeBasedSearchStats.perDayOfWeek;\n\n\n var numIndexers = $scope.avgResponseTimes.length;\n\n $scope.avgResponseTimesChart = getChart(\"multiBarHorizontalChart\", $scope.avgResponseTimes, \"name\", \"avgResponseTime\", \"\", \"Response time\");\n $scope.avgResponseTimesChart.options.chart.margin.left = 100;\n $scope.avgResponseTimesChart.options.chart.yAxis.rotateLabels = -30;\n var avgResponseTimesChartHeight = Math.max(numIndexers * 30, 350);\n $scope.avgResponseTimesChart.options.chart.height = avgResponseTimesChartHeight;\n\n $scope.resultsSharesChart = getResultsSharesChart();\n\n var rotation = 30;\n if (numIndexers > 30) {\n rotation = 70;\n }\n $scope.resultsSharesChart.options.chart.xAxis.rotateLabels = rotation;\n $scope.resultsSharesChart.options.chart.height = avgResponseTimesChartHeight;\n\n $scope.downloadsPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.downloadsPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Downloads');\n $scope.downloadsPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.downloadsPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.downloadsPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Downloads');\n $scope.downloadsPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.searchesPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.searchesPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Searches');\n $scope.searchesPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.searchesPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.searchesPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Searches');\n $scope.searchesPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\n\n $scope.indexerDownloadSharesChart = {\n options: {\n chart: {\n type: 'pieChart',\n height: 500,\n x: function (d) {\n return d.name;\n },\n y: function (d) {\n return d.share;\n },\n showLabels: true,\n duration: 500,\n labelThreshold: 0.01,\n labelSunbeamLayout: true,\n tooltip: {\n valueFormatter: function (d, i) {\n return $filter('number')(d, 2) + \"%\";\n }\n },\n legend: {\n margin: {\n top: 5,\n right: 35,\n bottom: 5,\n left: 0\n }\n }\n }\n },\n data: $scope.indexerDownloadShares\n };\n\n $scope.indexerDownloadSharesChart.options.chart.height = Math.min(Math.max(numIndexers * 40, 350), 900);\n };\n\n $scope.setStats(stats);\n\n\n function getChart(chartType, values, xKey, yKey, xAxisLabel, yAxisLabel) {\n return {\n options: {\n chart: {\n type: chartType,\n height: 350,\n margin: {\n top: 20,\n right: 20,\n bottom: 100,\n left: 50\n },\n x: function (d) {\n return d[xKey];\n },\n y: function (d) {\n return d[yKey];\n },\n showValues: true,\n valueFormat: function (d) {\n return d;\n },\n color: function () {\n return \"red\"\n },\n showControls: false,\n showLegend: false,\n duration: 100,\n xAxis: {\n axisLabel: xAxisLabel,\n tickFormat: function (d) {\n return d;\n },\n rotateLabels: 30,\n showMaxMin: false,\n color: function () {\n return \"white\"\n }\n },\n yAxis: {\n axisLabel: yAxisLabel,\n axisLabelDistance: -10,\n tickFormat: function (d) {\n return d;\n }\n },\n tooltip: {\n enabled: false\n },\n zoom: {\n enabled: true,\n scaleExtent: [1, 10],\n useFixedDomain: false,\n useNiceScale: false,\n horizontalOff: false,\n verticalOff: true,\n unzoomEventType: 'dblclick.zoom'\n }\n }\n }, data: [{\n \"key\": \"doesntmatter\",\n \"bar\": true,\n \"values\": values\n }]\n };\n }\n\n //Was unable to use the function above for this and gave up\n function getResultsSharesChart() {\n return {\n options: {\n chart: {\n type: 'multiBarChart',\n height: 350,\n margin: {\n top: 20,\n right: 20,\n bottom: 100,\n left: 45\n },\n\n clipEdge: true,\n duration: 500,\n stacked: false,\n reduceXTicks: false,\n showValues: true,\n tooltip: {\n enabled: true,\n valueFormatter: function (d) {\n return d + \"%\";\n }\n },\n showControls: false,\n xAxis: {\n axisLabel: '',\n showMaxMin: false,\n rotateLabels: 30,\n axisLabelDistance: 30,\n tickFormat: function (d) {\n return d;\n }\n },\n yAxis: {\n axisLabel: 'Share (%)',\n axisLabelDistance: -20,\n tickFormat: function (d) {\n return d;\n }\n }\n }\n },\n\n data: [\n {\n key: \"Results\",\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\n return {series: 0, y: stats.avgResultsShare, x: stats.name}\n })\n },\n {\n key: \"Unique results\",\n values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {\n return {series: 1, y: stats.avgUniqueResults, x: stats.name}\n })\n }\n ]\n };\n }\n\n\n}\nStatsController.$inject = [\"$scope\", \"$filter\", \"StatsService\", \"blockUI\", \"stats\"];\n","//\nangular\n .module('nzbhydraApp')\n .factory('SearchService', SearchService);\n\nfunction SearchService($http) {\n\n\n var lastExecutedQuery;\n var lastResults;\n\n return {\n search: search,\n getLastResults: getLastResults,\n loadMore: loadMore\n };\n \n\n function search(category, query, tmdbid, title, tvdbid, rid, season, episode, minsize, maxsize, minage, maxage, indexers, mode) {\n var uri;\n if (category.indexOf(\"Movies\") > -1 || (category.indexOf(\"20\") == 0) || mode == \"movie\") {\n console.log(\"Search for movies\");\n uri = new URI(\"internalapi/moviesearch\");\n if (angular.isDefined(tmdbid)) {\n console.log(\"moviesearch per tmdbid\");\n uri.addQuery(\"tmdbid\", tmdbid);\n } else {\n console.log(\"moviesearch per query\");\n uri.addQuery(\"query\", query);\n }\n\n } else if (category.indexOf(\"TV\") > -1 || (category.indexOf(\"50\") == 0) || mode == \"tvsearch\") {\n console.log(\"Search for shows\");\n uri = new URI(\"internalapi/tvsearch\");\n if (angular.isDefined(tvdbid)) {\n uri.addQuery(\"tvdbid\", tvdbid);\n }\n if (angular.isDefined(rid)) {\n uri.addQuery(\"rid\", rid);\n } else {\n console.log(\"tvsearch per query\");\n uri.addQuery(\"query\", query);\n }\n\n if (angular.isDefined(season)) {\n uri.addQuery(\"season\", season);\n }\n if (angular.isDefined(episode)) {\n uri.addQuery(\"episode\", episode);\n }\n } else {\n uri = new URI(\"internalapi/search\");\n uri.addQuery(\"query\", query);\n }\n if (angular.isDefined(title)) {\n uri.addQuery(\"title\", title);\n }\n if (_.isNumber(minsize)) {\n uri.addQuery(\"minsize\", minsize);\n }\n if (_.isNumber(maxsize)) {\n uri.addQuery(\"maxsize\", maxsize);\n }\n if (_.isNumber(minage)) {\n uri.addQuery(\"minage\", minage);\n }\n if (_.isNumber(maxage)) {\n uri.addQuery(\"maxage\", maxage);\n }\n if (!angular.isUndefined(indexers)) {\n uri.addQuery(\"indexers\", decodeURIComponent(indexers));\n }\n \n\n uri.addQuery(\"category\", category);\n lastExecutedQuery = uri;\n return $http.get(uri.toString()).then(processData);\n\n }\n\n function loadMore(offset, loadAll) {\n lastExecutedQuery.removeQuery(\"offset\");\n lastExecutedQuery.addQuery(\"offset\", offset);\n lastExecutedQuery.addQuery(\"loadAll\", loadAll ? true : false);\n\n return $http.get(lastExecutedQuery.toString()).then(processData);\n }\n\n function processData(response) {\n var results = response.data.results;\n var indexersearches = response.data.indexersearches;\n var total = response.data.total;\n var rejected = response.data.rejected;\n var resultsCount = results.length;\n\n\n //Sum up response times of indexers from individual api accesses\n //TODO: Move this to search result controller because we need to update it every time we loaded more results\n _.each(indexersearches, function (ps) {\n if (ps.did_search) {\n ps.averageResponseTime = _.reduce(ps.apiAccesses, function (memo, rp) {\n return memo + rp.response_time;\n }, 0);\n ps.averageResponseTime = ps.averageResponseTime / ps.apiAccesses.length;\n }\n });\n \n lastResults = {\"results\": results, \"indexersearches\": indexersearches, \"total\": total, \"resultsCount\": resultsCount, \"rejected\": rejected};\n return lastResults;\n }\n \n function getLastResults() {\n return lastResults;\n }\n}\nSearchService.$inject = [\"$http\"];","angular\n .module('nzbhydraApp')\n .controller('SearchResultsController', SearchResultsController);\n\nfunction sumRejected(rejected) {\n return _.reduce(rejected, function (memo, entry) {\n return memo + entry[1];\n }, 0);\n}\n\n//SearchResultsController.$inject = ['blockUi'];\nfunction SearchResultsController($stateParams, $scope, $q, $timeout, blockUI, growl, localStorageService, SearchService, ConfigService) {\n\n if (localStorageService.get(\"sorting\") != null) {\n var sorting = localStorageService.get(\"sorting\");\n $scope.sortPredicate = sorting.predicate;\n $scope.sortReversed = sorting.reversed;\n } else {\n $scope.sortPredicate = \"epoch\";\n $scope.sortReversed = true;\n }\n $scope.limitTo = 100;\n $scope.offset = 0;\n //Handle incoming data\n\n $scope.indexersearches = _.sortBy(SearchService.getLastResults().indexersearches, function (i) {\n return i.indexer.toLowerCase()\n });\n $scope.indexerDisplayState = []; //Stores if a indexer's results should be displayed or not\n $scope.indexerResultsInfo = {}; //Stores information about the indexer's results like how many we already retrieved\n $scope.groupExpanded = {};\n $scope.selected = [];\n $scope.lastClicked = null;\n $scope.lastClickedValue = null;\n\n $scope.foo = {\n indexerStatusesExpanded: localStorageService.get(\"indexerStatusesExpanded\") != null ? localStorageService.get(\"indexerStatusesExpanded\") : false,\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") != null ? localStorageService.get(\"duplicatesDisplayed\") : false\n };\n\n $scope.countFilteredOut = 0;\n\n //Initially set visibility of all found indexers to true, they're needed for initial filtering / sorting\n _.forEach($scope.indexersearches, function (ps) {\n $scope.indexerDisplayState[ps.indexer.toLowerCase()] = true;\n });\n\n _.forEach($scope.indexersearches, function (ps) {\n $scope.indexerResultsInfo[ps.indexer.toLowerCase()] = {loadedResults: ps.loaded_results};\n });\n\n //Process results\n $scope.results = SearchService.getLastResults().results;\n $scope.total = SearchService.getLastResults().total;\n $scope.resultsCount = SearchService.getLastResults().resultsCount;\n $scope.rejected = SearchService.getLastResults().rejected;\n $scope.countRejected = sumRejected($scope.rejected);\n $scope.filteredResults = sortAndFilter($scope.results);\n stopBlocking();\n\n //Returns the content of the property (defined by the current sortPredicate) of the first group element \n $scope.firstResultPredicate = firstResultPredicate;\n function firstResultPredicate(item) {\n return item[0][$scope.sortPredicate];\n }\n\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...) \n $scope.groupId = groupId;\n function groupId(item) {\n return item[0][0].searchResultId;\n }\n\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?\n function startBlocking(message) {\n var deferred = $q.defer();\n blockUI.start(message);\n $timeout(function () {\n deferred.resolve();\n }, 100);\n return deferred.promise;\n }\n\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.)\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 \n $scope.setSorting = setSorting;\n function setSorting(predicate, reversedDefault) {\n if (predicate == $scope.sortPredicate) {\n $scope.sortReversed = !$scope.sortReversed;\n } else {\n $scope.sortReversed = reversedDefault;\n }\n $scope.sortPredicate = predicate;\n startBlocking(\"Sorting / filtering...\").then(function () {\n $scope.filteredResults = sortAndFilter($scope.results);\n blockUI.reset();\n localStorageService.set(\"sorting\", {predicate: predicate, reversed: $scope.sortReversed});\n });\n }\n\n $scope.inlineFilter = inlineFilter;\n function inlineFilter(result) {\n var ok = true;\n ok = ok && $scope.titleFilter && result.title.toLowerCase().indexOf($scope.titleFilter) > -1;\n ok = ok && $scope.minSizeFilter && $scope.minSizeFilter * 1024 * 1024 < result.size;\n ok = ok && $scope.maxSizeFilter && $scope.maxSizeFilter * 1024 * 1024 > result.size;\n return ok;\n }\n\n\n $scope.$on(\"searchInputChanged\", function (event, query, minage, maxage, minsize, maxsize) {\n console.log(\"Got event searchInputChanged\");\n $scope.filteredResults = sortAndFilter($scope.results, query, minage, maxage, minsize, maxsize);\n });\n\n $scope.resort = function () {\n };\n\n function sortAndFilter(results, query, minage, maxage, minsize, maxsize) {\n $scope.countFilteredOut = 0;\n\n function filterByAgeAndSize(item) {\n var ok = true;\n ok = ok && (!_.isNumber(minsize) || item.size / 1024 / 1024 >= minsize)\n && (!_.isNumber(maxsize) || item.size / 1024 / 1024 <= maxsize)\n && (!_.isNumber(minage) || item.age_days >= Number(minage))\n && (!_.isNumber(maxage) || item.age_days <= Number(maxage));\n\n if (ok && query) {\n var words = query.toLowerCase().split(\" \");\n ok = _.every(words, function (word) {\n return item.title.toLowerCase().indexOf(word) > -1;\n });\n }\n if (!ok) {\n $scope.countFilteredOut++;\n }\n return ok;\n }\n\n\n function getItemIndexerDisplayState(item) {\n return $scope.indexerDisplayState[item.indexer.toLowerCase()];\n }\n\n function getCleanedTitle(element) {\n return element.title.toLowerCase().replace(/[\\s\\-\\._]/ig, \"\");\n }\n\n function createSortedHashgroups(titleGroup) {\n\n function createHashGroup(hashGroup) {\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)\n var sortedHashGroup = _.sortBy(hashGroup, function (item) {\n var sortPredicateValue;\n if ($scope.sortPredicate == \"grabs\") {\n sortPredicateValue = angular.isDefined(item.grabs) ? item.grabs : 0;\n } else {\n sortPredicateValue = item[$scope.sortPredicate];\n }\n //var sortPredicateValue = item[$scope.sortPredicate];\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\n });\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)\n sortedHashGroup = _.sortBy(sortedHashGroup, function (item) {\n return item.indexerscore * -1;\n });\n return sortedHashGroup;\n }\n\n function getHashGroupFirstElementSortPredicate(hashGroup) {\n if ($scope.sortPredicate == \"grabs\") {\n sortPredicateValue = angular.isDefined(hashGroup[0].grabs) ? hashGroup[0].grabs : 0;\n } else {\n var sortPredicateValue = hashGroup[0][$scope.sortPredicate];\n }\n return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;\n }\n\n return _.chain(titleGroup).groupBy(\"hash\").map(createHashGroup).sortBy(getHashGroupFirstElementSortPredicate).value();\n }\n\n function getTitleGroupFirstElementsSortPredicate(titleGroup) {\n var sortPredicateValue;\n if ($scope.sortPredicate == \"title\") {\n sortPredicateValue = titleGroup[0][0].title.toLowerCase();\n } else if ($scope.sortPredicate == \"grabs\") {\n sortPredicateValue = angular.isDefined(titleGroup[0][0].grabs) ? titleGroup[0][0].grabs : 0;\n } else {\n sortPredicateValue = titleGroup[0][0][$scope.sortPredicate];\n }\n\n return sortPredicateValue;\n }\n\n var filtered = _.chain(results)\n //Filter by age, size and title\n .filter(filterByAgeAndSize)\n //Remove elements of which the indexer is currently hidden \n .filter(getItemIndexerDisplayState)\n //Make groups of results with the same title \n .groupBy(getCleanedTitle)\n //For every title group make subgroups of duplicates and sort the group \n .map(createSortedHashgroups)\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) \n .sortBy(getTitleGroupFirstElementsSortPredicate)\n .value();\n if ($scope.sortReversed) {\n filtered = filtered.reverse();\n }\n if ($scope.countFilteredOut > 0) {\n growl.info(\"Filtered \" + $scope.countFilteredOut + \" of the retrieved results\");\n }\n\n $scope.lastClicked = null;\n return filtered;\n }\n\n $scope.toggleTitlegroupExpand = function toggleTitlegroupExpand(titleGroup) {\n $scope.groupExpanded[titleGroup[0][0].title] = !$scope.groupExpanded[titleGroup[0][0].title];\n $scope.groupExpanded[titleGroup[0][0].hash] = !$scope.groupExpanded[titleGroup[0][0].hash];\n };\n\n\n $scope.stopBlocking = stopBlocking;\n function stopBlocking() {\n blockUI.reset();\n }\n\n $scope.loadMore = loadMore;\n function loadMore(loadAll) {\n startBlocking(loadAll ? \"Loading all results...\" : \"Loading more results...\").then(function () {\n SearchService.loadMore($scope.resultsCount, loadAll).then(function (data) {\n $scope.results = $scope.results.concat(data.results);\n $scope.filteredResults = sortAndFilter($scope.results);\n $scope.total = data.total;\n $scope.rejected = data.rejected;\n $scope.countRejected = sumRejected($scope.rejected);\n $scope.resultsCount += data.resultsCount;\n stopBlocking();\n });\n });\n }\n\n\n//Filters the results according to new visibility settings.\n $scope.toggleIndexerDisplay = toggleIndexerDisplay;\n function toggleIndexerDisplay(indexer) {\n $scope.indexerDisplayState[indexer.toLowerCase()] = $scope.indexerDisplayState[indexer.toLowerCase()];\n startBlocking(\"Filtering. Sorry...\").then(function () {\n $scope.filteredResults = sortAndFilter($scope.results);\n }).then(function () {\n stopBlocking();\n });\n }\n\n $scope.countResults = countResults;\n function countResults() {\n return $scope.results.length;\n }\n\n $scope.invertSelection = function invertSelection() {\n $scope.$broadcast(\"invertSelection\");\n };\n\n $scope.toggleIndexerStatuses = function () {\n $scope.foo.indexerStatusesExpanded = !$scope.foo.indexerStatusesExpanded;\n localStorageService.set(\"indexerStatusesExpanded\", $scope.foo.indexerStatusesExpanded);\n };\n\n $scope.toggleDuplicatesDisplayed = function () {\n //$scope.foo.duplicatesDisplayed = !$scope.foo.duplicatesDisplayed;\n localStorageService.set(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\n $scope.$broadcast(\"duplicatesDisplayed\", $scope.foo.duplicatesDisplayed);\n };\n\n $scope.$on(\"checkboxClicked\", function (event, originalEvent, rowIndex, newCheckedValue) {\n if (originalEvent.shiftKey && $scope.lastClicked != null) {\n console.log(\"Shift clicked from \" + $scope.lastClicked + \" to \" + rowIndex);\n $scope.$broadcast(\"shiftClick\", Number($scope.lastClicked), Number(rowIndex), Number($scope.lastClickedValue));\n }\n $scope.lastClicked = rowIndex;\n $scope.lastClickedValue = newCheckedValue;\n })\n\n $scope.filterRejectedZero = function() {\n return function (entry) {\n return entry[1] > 0;\n }\n }\n}\nSearchResultsController.$inject = [\"$stateParams\", \"$scope\", \"$q\", \"$timeout\", \"blockUI\", \"growl\", \"localStorageService\", \"SearchService\", \"ConfigService\"];\n\n","angular\n .module('nzbhydraApp')\n .controller('SearchHistoryController', SearchHistoryController);\n\n\nfunction SearchHistoryController($scope, $state, StatsService, history, $sce, $filter) {\n $scope.type = \"All\";\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n $scope.isLoaded = true;\n $scope.searchRequests = history.data.searchRequests;\n $scope.totalRequests = history.data.totalRequests;\n\n $scope.pageChanged = function (newPage) {\n getSearchRequestsPage(newPage);\n };\n\n $scope.changeType = function (type) {\n $scope.type = type;\n getSearchRequestsPage($scope.pagination.current);\n };\n\n function getSearchRequestsPage(pageNumber) {\n StatsService.getSearchHistory(pageNumber, $scope.limit, $scope.type).then(function (history) {\n $scope.searchRequests = history.data.searchRequests;\n $scope.totalRequests = history.data.totalRequests;\n $scope.isLoaded = true;\n });\n }\n\n $scope.openSearch = function (request) {\n var stateParams = {};\n if (request.identifier_key == \"imdbid\") {\n stateParams.imdbid = request.identifier_value;\n } else if (request.identifier_key == \"tvdbid\" || request.identifier_key == \"rid\") {\n if (request.identifier_key == \"rid\") {\n stateParams.rid = request.identifier_value;\n } else {\n stateParams.tvdbid = request.identifier_value;\n }\n\n if (request.season != \"\") {\n stateParams.season = request.season;\n }\n if (request.episode != \"\") {\n stateParams.episode = request.episode;\n }\n }\n if (request.query != \"\") {\n stateParams.query = request.query;\n }\n if (request.type == \"tv\") {\n stateParams.mode = \"tvsearch\"\n } else if (request.type == \"tv\") {\n stateParams.mode = \"movie\"\n } else {\n stateParams.mode = \"search\"\n }\n\n if (request.movietitle != null) {\n stateParams.title = request.movietitle;\n }\n if (request.tvtitle != null) {\n stateParams.title = request.tvtitle;\n }\n\n if (request.category) {\n stateParams.category = request.category;\n }\n\n stateParams.category = request.category;\n\n $state.go(\"root.search\", stateParams, {inherit: false});\n };\n\n $scope.formatQuery = function (request) {\n if (request.movietitle != null) {\n return request.movietitle;\n }\n if (request.tvtitle != null) {\n return request.tvtitle;\n }\n\n if (!request.query && !request.identifier_key && !request.season && !request.episode) {\n return \"Update query\";\n }\n return request.query;\n };\n\n $scope.formatAdditional = function(request) {\n var result = [];\n //ID key: ID value\n //season\n //episode\n //author\n //title\n if (request.identifier_key) {\n var href;\n var key;\n if (request.identifier_key == \"imdbid\") {\n key = \"IMDB ID\";\n href = \"https://www.imdb.com/title/tt\"\n } else if (request.identifier_key == \"tvdbid\") {\n key = \"TVDB ID\";\n href = \"https://thetvdb.com/?tab=series&id=\"\n } else if (request.identifier_key == \"rid\") {\n key = \"TVRage ID\";\n href = \"internalapi/redirect_rid?rid=\"\n } else if (request.identifier_key == \"tmdb\") {\n key = \"TMDV ID\";\n href = \"https://www.themoviedb.org/movie/\"\n }\n href = href + request.identifier_value;\n href = $filter(\"dereferer\")(href);\n result.push(key + \": \" + '' + request.identifier_value + \"\");\n }\n if (request.season) {\n result.push(\"Season: \" + request.season);\n }\n if (request.episode) {\n result.push(\"Episode: \" + request.episode);\n }\n if (request.author) {\n result.push(\"Author: \" + request.author);\n }\n if (request.title) {\n result.push(\"Title: \" + request.title);\n }\n return $sce.trustAsHtml(result.join(\", \"));\n };\n\n\n}\nSearchHistoryController.$inject = [\"$scope\", \"$state\", \"StatsService\", \"history\", \"$sce\", \"$filter\"];\n","angular\n .module('nzbhydraApp')\n .controller('SearchController', SearchController);\n\nfunction SearchController($scope, $http, $stateParams, $state, $window, $filter, SearchService, focus, ConfigService, CategoriesService, blockUI, $element, ModalService) {\n\n function getNumberOrUndefined(number) {\n if (_.isUndefined(number) || _.isNaN(number) || number == \"\") {\n return undefined;\n }\n number = parseInt(number);\n if (_.isNumber(number)) {\n return number;\n } else {\n return undefined;\n }\n }\n\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)\n $scope.mode = $stateParams.mode;\n $scope.categories = _.filter(CategoriesService.getAll(), function (c) {\n return c.mayBeSelected && c.ignoreResults != \"internal\" && c.ignoreResults != \"always\";\n });\n if (angular.isDefined($stateParams.category) && $stateParams.category) {\n $scope.category = CategoriesService.getByName($stateParams.category);\n } else {\n $scope.category = CategoriesService.getDefault();\n }\n $scope.category = (_.isUndefined($stateParams.category) || $stateParams.category == \"\") ? CategoriesService.getDefault() : CategoriesService.getByName($stateParams.category);\n $scope.tmdbid = $stateParams.tmdbid;\n $scope.tvdbid = $stateParams.tvdbid;\n $scope.rid = $stateParams.rid;\n $scope.title = $stateParams.title;\n $scope.season = $stateParams.season;\n $scope.episode = $stateParams.episode;\n $scope.query = $stateParams.query;\n $scope.minsize = getNumberOrUndefined($stateParams.minsize);\n $scope.maxsize = getNumberOrUndefined($stateParams.maxsize);\n $scope.minage = getNumberOrUndefined($stateParams.minage);\n $scope.maxage = getNumberOrUndefined($stateParams.maxage);\n if (!_.isUndefined($scope.title) && _.isUndefined($scope.query)) {\n //$scope.query = $scope.title;\n }\n if (!angular.isUndefined($stateParams.indexers)) {\n $scope.indexers = decodeURIComponent($stateParams.indexers).split(\"|\");\n }\n\n $scope.showIndexers = {};\n\n var safeConfig = ConfigService.getSafe();\n\n\n\n\n $scope.typeAheadWait = 300;\n $scope.selectedItem = \"\";\n $scope.autocompleteLoading = false;\n $scope.isAskById = $scope.category.supportsById;\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\n $scope.availableIndexers = [];\n $scope.autocompleteClass = \"autocompletePosterMovies\";\n\n $scope.toggle = function (searchCategory) {\n $scope.category = searchCategory;\n\n //Show checkbox to ask if the user wants to search by ID (using autocomplete)\n $scope.isAskById = $scope.category.supportsById;\n\n focus('focus-query-box');\n\n //Hacky way of triggering the autocomplete loading\n var searchModel = $element.find(\"#searchfield\").controller(\"ngModel\");\n if (angular.isDefined(searchModel.$viewValue)) {\n searchModel.$setViewValue(searchModel.$viewValue + \" \");\n }\n\n if (safeConfig.searching.enableCategorySizes) {\n var min = searchCategory.min;\n var max = searchCategory.max;\n if (_.isNumber(min)) {\n $scope.minsize = min;\n } else {\n $scope.minsize = \"\";\n }\n if (_.isNumber(max)) {\n $scope.maxsize = max;\n } else {\n $scope.maxsize = \"\";\n }\n }\n\n $scope.availableIndexers = getAvailableIndexers();\n\n\n };\n\n\n // Any function returning a promise object can be used to load values asynchronously\n $scope.getAutocomplete = function (val) {\n $scope.autocompleteLoading = true;\n //Expected model returned from API:\n //label: What to show in the results\n //title: Will be used for file search\n //value: Will be used as extraInfo (ttid oder tvdb id)\n //poster: url of poster to show\n\n //Don't use autocomplete if checkbox is disabled\n if (!$scope.isById.value) {\n return {};\n }\n\n if ($scope.category.name.indexOf(\"movies\") > -1) {\n return $http.get('internalapi/autocomplete?type=movie', {\n params: {\n input: val\n }\n }).then(function (response) {\n $scope.autocompleteLoading = false;\n return response.data.results;\n });\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\n\n return $http.get('internalapi/autocomplete?type=tv', {\n params: {\n input: val\n }\n }).then(function (response) {\n $scope.autocompleteLoading = false;\n return response.data.results;\n });\n } else {\n return {};\n }\n };\n\n\n $scope.startSearch = function () {\n blockUI.start(\"Searching...\");\n var indexers = angular.isUndefined($scope.indexers) ? undefined : $scope.indexers.join(\"|\");\n SearchService.search($scope.category.name, $scope.query, $stateParams.tmdbid, $scope.title, $scope.tvdbid, $scope.rid, $scope.season, $scope.episode, $scope.minsize, $scope.maxsize, $scope.minage, $scope.maxage, indexers, $scope.mode).then(function () {\n $state.go(\"root.search.results\", {\n minsize: $scope.minsize,\n maxsize: $scope.maxsize,\n minage: $scope.minage,\n maxage: $scope.maxage\n }, {\n inherit: true\n });\n $scope.tmdbid = undefined;\n $scope.tvdbid = undefined;\n });\n };\n\n function getSelectedIndexers() {\n var activatedIndexers = _.filter($scope.availableIndexers).filter(function (indexer) {\n return indexer.activated;\n });\n return _.pluck(activatedIndexers, \"name\").join(\"|\");\n }\n\n\n $scope.goToSearchUrl = function () {\n var stateParams = {};\n if ($scope.category.name.indexOf(\"movies\") > -1) {\n stateParams.title = $scope.title;\n stateParams.mode = \"movie\";\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\n stateParams.mode = \"tvsearch\";\n stateParams.title = $scope.title;\n } else if ($scope.category.name == \"ebook\") {\n stateParams.mode = \"ebook\";\n } else {\n stateParams.mode = \"search\";\n }\n\n stateParams.tmdbid = $scope.tmdbid;\n stateParams.tvdbid = $scope.tvdbid;\n stateParams.title = $scope.title;\n stateParams.season = $scope.season;\n stateParams.episode = $scope.episode;\n stateParams.query = $scope.query;\n stateParams.minsize = $scope.minsize;\n stateParams.maxsize = $scope.maxsize;\n stateParams.minage = $scope.minage;\n stateParams.maxage = $scope.maxage;\n stateParams.category = $scope.category.name;\n stateParams.indexers = encodeURIComponent(getSelectedIndexers());\n\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\n };\n\n\n $scope.selectAutocompleteItem = function ($item) {\n $scope.selectedItem = $item;\n $scope.title = $item.title;\n if ($scope.category.name.indexOf(\"movies\") > -1) {\n $scope.tmdbid = $item.value;\n } else if ($scope.category.name.indexOf(\"tv\") > -1) {\n $scope.tvdbid = $item.value;\n }\n $scope.query = \"\";\n $scope.goToSearchUrl();\n };\n\n $scope.startQuerySearch = function () {\n //Reset values because they might've been set from the last search\n $scope.title = undefined;\n $scope.tmdbid = undefined;\n $scope.tvdbid = undefined;\n $scope.goToSearchUrl();\n };\n\n\n $scope.autocompleteActive = function () {\n return $scope.category.supportsById;\n };\n\n $scope.seriesSelected = function () {\n return $scope.category.name.indexOf(\"tv\") > -1;\n };\n\n $scope.toggleIndexer = function (indexer) {\n $scope.indexers[indexer] = !$scope.indexers[indexer]\n };\n\n\n function isIndexerPreselected(indexer) {\n if (angular.isUndefined($scope.indexers)) {\n return indexer.preselect;\n } else {\n return _.contains($scope.indexers, indexer.name);\n }\n\n }\n\n\n function getAvailableIndexers() {\n return _.chain(safeConfig.indexers).filter(function (indexer) {\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);\n }).sortBy(function (indexer) {\n return indexer.name.toLowerCase();\n })\n .map(function (indexer) {\n return {name: indexer.name, activated: isIndexerPreselected(indexer), categories: indexer.categories};\n }).value();\n }\n\n $scope.toggleAllIndexers = function () {\n angular.forEach($scope.availableIndexers, function (indexer) {\n indexer.activated = !indexer.activated;\n })\n };\n\n $scope.searchInputChanged = function () {\n $scope.$broadcast(\"searchInputChanged\", $scope.query != $stateParams.query ? $scope.query : null, $scope.minage, $scope.maxage, $scope.minsize, $scope.maxsize);\n };\n\n $scope.availableIndexers = getAvailableIndexers();\n\n\n if ($scope.mode) {\n $scope.startSearch();\n }\n\n}\nSearchController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\", \"$window\", \"$filter\", \"SearchService\", \"focus\", \"ConfigService\", \"CategoriesService\", \"blockUI\", \"$element\", \"ModalService\"];\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\n .module('nzbhydraApp')\n .factory('NzbDownloadService', NzbDownloadService);\n\nfunction NzbDownloadService($http, ConfigService, DownloaderCategoriesService) {\n\n var service = {\n download: download,\n getEnabledDownloaders: getEnabledDownloaders\n };\n\n return service;\n\n function sendNzbAddCommand(downloader, searchresultids, category) {\n var params = {downloader: downloader.name, searchresultids: angular.toJson(searchresultids)};\n if (category != \"No category\") {\n params[\"category\"] = category;\n }\n return $http.put(\"internalapi/addnzbs\", params);\n }\n \n function download(downloader, searchresultids) {\n \n var category = downloader.defaultCategory;\n \n if ((_.isUndefined(category) || category == \"\" || category == null) && category != \"No category\") {\n return DownloaderCategoriesService.openCategorySelection(downloader).then(function (category) {\n return sendNzbAddCommand(downloader, searchresultids, category)\n }, function (error) {\n throw error;\n });\n } else {\n return sendNzbAddCommand(downloader, searchresultids, category)\n }\n }\n \n function getEnabledDownloaders() {\n return _.filter(ConfigService.getSafe().downloaders, \"enabled\");\n }\n}\nNzbDownloadService.$inject = [\"$http\", \"ConfigService\", \"DownloaderCategoriesService\"];\n\n","angular\n .module('nzbhydraApp')\n .factory('ModalService', ModalService);\n\nfunction ModalService($uibModal, $q) {\n \n return {\n open: open\n };\n \n function open(headline, message, params, size) {\n //params example:\n /*\n var p =\n {\n yes: {\n text: \"Yes\", //default: Ok\n onYes: function() {}\n },\n no: { //default: Empty\n text: \"No\",\n onNo: function () {\n }\n },\n cancel: { \n text: \"Cancel\", //default: Cancel\n onCancel: function () {\n }\n }\n };\n */\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/modal.html',\n controller: 'ModalInstanceCtrl',\n size: angular.isDefined(size) ? size : \"md\",\n resolve: {\n headline: function () {\n return headline;\n },\n message: function(){ \n return message;\n },\n params: function() {\n return params;\n }\n }\n });\n\n modalInstance.result.then(function() {\n \n }, function() {\n \n });\n }\n \n}\nModalService.$inject = [\"$uibModal\", \"$q\"];\n\nangular\n .module('nzbhydraApp')\n .controller('ModalInstanceCtrl', ModalInstanceCtrl);\n\nfunction ModalInstanceCtrl($scope, $uibModalInstance, headline, message, params) {\n\n $scope.message = message;\n $scope.headline = headline;\n $scope.params = params;\n $scope.showCancel = angular.isDefined(params) && angular.isDefined(params.cancel);\n $scope.showNo = angular.isDefined(params) && angular.isDefined(params.no);\n\n if (angular.isUndefined(params) || angular.isUndefined(params.yes)) {\n $scope.params = {\n yes: {\n text: \"Ok\"\n }\n }\n } else if (angular.isUndefined(params.yes.text)) {\n params.yes.text = \"Yes\";\n }\n \n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isUndefined($scope.params.no.text)) {\n $scope.params.no.text = \"No\";\n }\n \n if (angular.isDefined(params) && angular.isDefined(params.cancel) && angular.isUndefined($scope.params.cancel.text)) {\n $scope.params.cancel.text = \"Cancel\";\n }\n\n $scope.yes = function () {\n $uibModalInstance.close();\n if(angular.isDefined(params) && angular.isDefined(params.yes) && angular.isDefined($scope.params.yes.onYes)) {\n $scope.params.yes.onYes();\n }\n };\n\n $scope.no = function () {\n $uibModalInstance.close();\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isDefined($scope.params.no.onNo)) {\n $scope.params.no.onNo();\n }\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n if (angular.isDefined(params.cancel) && angular.isDefined($scope.params.cancel.onCancel)) {\n $scope.params.cancel.onCancel();\n }\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason, c) {\n if (reason == \"backdrop click\") {\n $scope.cancel();\n }\n });\n}\nModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"headline\", \"message\", \"params\"];\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\n .module('nzbhydraApp')\n .controller('LoginController', LoginController);\n\nfunction LoginController($scope, RequestsErrorHandler, $state, HydraAuthService, $auth, growl) {\n $scope.user = {};\n $scope.login = function () {\n RequestsErrorHandler.specificallyHandled(function () {\n $auth.login($scope.user).then(function (data) {\n HydraAuthService.setLoggedInByForm();\n growl.info(\"Login successful!\");\n $state.go(\"root.search\");\n }, function () {\n growl.error(\"Login failed!\")\n });\n });\n }\n}\nLoginController.$inject = [\"$scope\", \"RequestsErrorHandler\", \"$state\", \"HydraAuthService\", \"$auth\", \"growl\"];\n","angular\n .module('nzbhydraApp')\n .controller('IndexerStatusesController', IndexerStatusesController);\n\n function IndexerStatusesController($scope, $http, statuses) {\n $scope.statuses = statuses.data.indexerStatuses;\n \n $scope.isInPast = function (timestamp) {\n return timestamp * 1000 < (new Date).getTime();\n };\n \n $scope.enable = function(indexerName) {\n $http.get(\"internalapi/enableindexer\", {params: {name: indexerName}}).then(function(response){\n $scope.statuses = response.data.indexerStatuses;\n });\n }\n\n }\n IndexerStatusesController.$inject = [\"$scope\", \"$http\", \"statuses\"];\n\n\nangular\n .module('nzbhydraApp')\n .filter('formatDate', formatDate);\n\nfunction formatDate(dateFilter) {\n return function(timestamp, hidePast) {\n if (timestamp) {\n if (timestamp * 1000 < (new Date).getTime() && hidePast) {\n return \"\"; //\n }\n \n var t = timestamp * 1000;\n t = dateFilter(t, 'yyyy-MM-dd HH:mm');\n return t;\n } else {\n return \"\";\n }\n }\n}\nformatDate.$inject = [\"dateFilter\"];\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDate', reformatDate);\n\nfunction reformatDate() {\n return function (date) {\n //Date in database is saved as UTC without timezone information\n return moment.utc(date, \"ddd, D MMM YYYY HH:mm:ss z\").local().format(\"YYYY-MM-DD HH:mm\");\n \n }\n}","angular\n .module('nzbhydraApp')\n .controller('IndexController', IndexController);\n\nfunction IndexController($scope, $http, $stateParams, $state) {\n console.log(\"Index\");\n $state.go(\"root.search\");\n}\nIndexController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\"];\n","angular\n .module('nzbhydraApp')\n .factory('HydraAuthService', HydraAuthService);\n\nfunction HydraAuthService($auth, $q, $rootScope, ConfigService, bootstrapped) {\n\n var loggedIn = false;\n var username;\n var maySeeAdmin = bootstrapped.maySeeAdmin;\n var maySeeStats = bootstrapped.maySeeStats;\n \n return {\n isLoggedIn: isLoggedIn,\n login: login,\n logout: logout,\n setLoggedInByForm: setLoggedInByForm,\n getUserRights: getUserRights,\n setLoggedInByBasic: setLoggedInByBasic,\n getUserName: getUserName\n };\n \n function isLoggedIn() {\n return loggedIn || (ConfigService.getSafe().authType == \"form\" && $auth.isAuthenticated()) || ConfigService.getSafe().authType == \"none\";\n }\n \n function setLoggedInByForm() {\n maySeeStats = $auth.getPayload().maySeeStats;\n maySeeAdmin = $auth.getPayload().maySeeAdmin;\n username = $auth.getPayload().username;\n loggedIn = true;\n $rootScope.$broadcast(\"user:loggedIn\", {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin});\n }\n\n function setLoggedInByBasic(_maySeeStats, _maySeeAdmin, _username) {\n maySeeAdmin = _maySeeAdmin;\n maySeeStats = _maySeeStats;\n username = _username;\n loggedIn = true;\n $rootScope.$broadcast(\"user:loggedIn\", {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin});\n }\n \n function login(user) {\n var deferred = $q.defer();\n $auth.login(user).then(function (data) {\n \n $rootScope.$broadcast(\"user:loggedIn\", data);\n deferred.resolve();\n });\n return deferred;\n }\n \n function logout() {\n $auth.logout();\n loggedIn = false;\n $rootScope.$broadcast(\"user:loggedOut\");\n }\n \n function getUserRights() {\n return {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin};\n }\n \n function getUserName() {\n return username;\n }\n \n \n \n \n}\nHydraAuthService.$inject = [\"$auth\", \"$q\", \"$rootScope\", \"ConfigService\", \"bootstrapped\"];","angular\n .module('nzbhydraApp')\n .controller('HeaderController', HeaderController);\n\nfunction HeaderController($scope, $state, $http, growl, HydraAuthService, ConfigService, bootstrapped) {\n\n $scope.showLoginout = false;\n\n if (ConfigService.getSafe().authType == \"none\") {\n $scope.showAdmin = true;\n $scope.showStats = true;\n $scope.showLoginout = false;\n } else {\n if (HydraAuthService.isLoggedIn()) {\n var rights = HydraAuthService.getUserRights();\n $scope.showAdmin = rights.maySeeAdmin;\n $scope.showStats = rights.maySeeStats;\n $scope.loginlogoutText = \"Logout\";\n $scope.showLoginout = true;\n } else {\n $scope.showAdmin = !bootstrapped.adminRestricted;\n $scope.showStats = !bootstrapped.statsRestricted;\n $scope.loginlogoutText = \"Login\";\n $scope.showLoginout = bootstrapped.adminRestricted || bootstrapped.statsRestricted || bootstrapped.searchRestricted;\n }\n }\n\n function onLogin(data) {\n $scope.showAdmin = data.maySeeAdmin;\n $scope.showStats = data.maySeeStats;\n $scope.showLoginout = true;\n $scope.loginlogoutText = \"Logout\";\n }\n\n $scope.$on(\"user:loggedIn\", function (event, data) {\n onLogin(data);\n });\n\n function onLogout() {\n $scope.showAdmin = !bootstrapped.adminRestricted;\n $scope.showStats = !bootstrapped.statsRestricted;\n $scope.loginlogoutText = \"Login\";\n $scope.showLoginout = bootstrapped.adminRestricted || bootstrapped.statsRestricted || bootstrapped.searchRestricted;\n }\n\n $scope.$on(\"user:loggedOut\", function (event, data) {\n onLogout();\n });\n\n $scope.loginout = function () {\n if (HydraAuthService.isLoggedIn()) {\n HydraAuthService.logout();\n\n if (ConfigService.getSafe().authType == \"basic\") {\n growl.info(\"Logged out. Close your browser to make sure session is closed.\");\n }\n else if (ConfigService.getSafe().authType == \"form\") {\n growl.info(\"Logged out\");\n }\n onLogout();\n $state.go(\"root.search\");\n } else {\n if (ConfigService.getSafe().authType == \"basic\") {\n var params = {};\n if (HydraAuthService.getUserName()) {\n params = {\n old_username: HydraAuthService.getUserName()\n }\n } \n $http.get(\"internalapi/askforpassword\", {params: params}).then(function () {\n growl.info(\"Login successful!\");\n //onLogin();\n $state.go(\"root.search\");\n })\n } else if (ConfigService.getSafe().authType == \"form\") {\n $state.go(\"root.login\");\n } else {\n growl.info(\"You shouldn't need to login but here you go!\");\n }\n }\n }\n}\nHeaderController.$inject = [\"$scope\", \"$state\", \"$http\", \"growl\", \"HydraAuthService\", \"ConfigService\", \"bootstrapped\"];\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) {\n return s.split(\"\").reduce(function (a, b) {\n a = ((a << 5) - a) + b.charCodeAt(0);\n return a & a\n }, 0);\n};\n\nangular\n .module('nzbhydraApp').run([\"formlyConfig\", \"formlyValidationMessages\", function (formlyConfig, formlyValidationMessages) {\n formlyValidationMessages.addStringMessage('required', 'This field is required');\n formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'fc.$touched || form.$submitted';\n\n}]);\n\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n formlyConfigProvider.extras.removeChromeAutoComplete = true;\n formlyConfigProvider.extras.explicitAsync = true;\n formlyConfigProvider.disableWarnings = window.onProd;\n\n\n formlyConfigProvider.setWrapper({\n name: 'settingWrapper',\n templateUrl: 'setting-wrapper.html'\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'fieldset',\n template: [\n '
',\n '{{options.templateOptions.label}}',\n '',\n '
'\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'help',\n template: [\n '
',\n '
',\n '
{{ line }}
',\n '
',\n '
'\n ].join(' ')\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'logicalGroup',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalInput',\n extends: 'input',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'timeOfDay',\n extends: 'horizontalInput',\n controller: ['$scope', function ($scope) {\n $scope.model[$scope.options.key] = moment.utc($scope.model[$scope.options.key]).toDate();\n }]\n });\n\n formlyConfigProvider.setType({\n name: 'percentInput',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'apiKeyInput',\n template: [\n '
',\n '',\n '',\n '',\n '
'\n ].join(' '),\n controller: function ($scope) {\n $scope.generate = function () {\n $scope.model[$scope.options.key] = (Math.random() * 1e32).toString(36);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'testConnection',\n templateUrl: 'button-test-connection.html'\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalTestConnection',\n extends: 'testConnection',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'checkCaps',\n templateUrl: 'button-check-caps.html',\n controller: function ($scope, ConfigBoxService) {\n $scope.message = \"\";\n $scope.uniqueId = hashCode($scope.model.name) + hashCode($scope.model.host);\n\n var testButton = \"#button-check-caps-\" + $scope.uniqueId;\n var testMessage = \"#message-check-caps-\" + $scope.uniqueId;\n\n function showSuccess() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).addClass(\"btn-success\");\n }\n\n function showError() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-danger\");\n }\n\n $scope.checkCaps = function () {\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\n\n var url = \"internalapi/test_caps\";\n var params = {indexer: $scope.model.name, apikey: $scope.model.apikey, host: $scope.model.host};\n ConfigBoxService.checkCaps(url, params, $scope.model).then(function (data, model) {\n angular.element(testMessage).text(\"Supports: \" + data.supportedIds + \",\" ? data.supportedIds && data.supportedTypes : \"\" + data.supportedTypes);\n showSuccess();\n }, function (message) {\n angular.element(testMessage).text(message);\n showError();\n }).finally(function () {\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\n });\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalCheckCaps',\n extends: 'checkCaps',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalApiKeyInput',\n extends: 'apiKeyInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalPercentInput',\n extends: 'percentInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'switch',\n template: \n '
'\n });\n\n\n formlyConfigProvider.setType({\n name: 'duoSetting',\n extends: 'input',\n defaultOptions: {\n className: 'col-md-9',\n templateOptions: {\n type: 'number',\n noRow: true,\n label: ''\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSwitch',\n extends: 'switch',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSelect',\n extends: 'select',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalMultiselect',\n defaultOptions: {\n templateOptions: {\n optionsAttr: 'bs-options',\n ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',\n valueProp: 'id',\n labelProp: 'label',\n getPlaceholder: function() {return \"\";}\n }\n },\n templateUrl: 'ui-select-multiple.html',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'label',\n template: ''\n });\n\n formlyConfigProvider.setType({\n name: 'duolabel',\n extends: 'label',\n defaultOptions: {\n className: 'col-md-2',\n templateOptions: {\n label: '-'\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'repeatSection',\n templateUrl: 'repeatSection.html',\n controller: function ($scope) {\n $scope.formOptions = {formState: $scope.formState};\n $scope.addNew = addNew;\n $scope.remove = remove;\n $scope.copyFields = copyFields;\n\n function copyFields(fields) {\n fields = angular.copy(fields);\n $scope.repeatfields = fields;\n return fields;\n }\n\n $scope.clear = function (field) {\n return _.mapObject(field, function (key, val) {\n if (typeof val === 'object') {\n return $scope.clear(val);\n }\n return undefined;\n\n });\n };\n\n\n function addNew() {\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\n var repeatsection = $scope.model[$scope.options.key];\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\n repeatsection.push(newsection);\n }\n\n function remove($index) {\n $scope.model[$scope.options.key].splice($index, 1);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'arrayConfig',\n templateUrl: 'arrayConfig.html',\n controller: function ($scope, $uibModal) {\n $scope.formOptions = {formState: $scope.formState};\n $scope._showBox = _showBox;\n $scope.showBox = showBox;\n $scope.isInitial = false;\n\n $scope.presets = $scope.options.data.presets;\n\n function _showBox(model, parentModel, isInitial, callback) {\n var modalInstance = $uibModal.open({\n templateUrl: 'configBox.html',\n controller: 'ConfigBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n return model;\n },\n fields: function () {\n return $scope.options.data.fieldsFunction(model, parentModel, isInitial, angular.injector());\n },\n isInitial: function () {\n return isInitial\n },\n parentModel: function () {\n return parentModel;\n },\n data: function () {\n return $scope.options.data;\n }\n }\n });\n\n\n modalInstance.result.then(function () {\n $scope.form.$setDirty(true);\n if (angular.isDefined(callback)) {\n callback(true);\n }\n }, function () {\n if (angular.isDefined(callback)) {\n callback(false);\n }\n });\n }\n\n function showBox(model, parentModel) {\n $scope._showBox(model, parentModel, false)\n }\n\n $scope.addEntry = function (entriesCollection, preset) {\n var model = angular.copy($scope.options.data.defaultModel);\n if (angular.isDefined(preset)) {\n _.extend(model, preset);\n }\n\n $scope.isInitial = true;\n\n $scope._showBox(model, entriesCollection, true, function (isSubmitted) {\n if (isSubmitted) {\n entriesCollection.push(model);\n }\n });\n };\n\n }\n\n });\n\n }]);\n\n\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) {\n\n $scope.model = model;\n $scope.fields = fields;\n $scope.isInitial = isInitial;\n $scope.allowDelete = data.allowDeleteFunction(model);\n $scope.spinnerActive = false;\n $scope.needsConnectionTest = false;\n \n $scope.obSubmit = function () {\n console.log($scope);\n if ($scope.form.$valid) {\n \n var a = data.checkBeforeClose($scope, model).then(function() {\n $uibModalInstance.close($scope);\n });\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n angular.forEach($scope.form.$error, function (error) {\n angular.forEach(error, function (field) {\n field.$setTouched();\n });\n });\n }\n };\n\n $scope.reset = function () {\n $scope.reset();\n };\n\n $scope.deleteEntry = function () {\n parentModel.splice(parentModel.indexOf(model), 1);\n $uibModalInstance.close($scope);\n };\n\n $scope.reset = function () {\n if (angular.isDefined(data.resetFunction)) {\n data.resetFunction($scope);\n }\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\n if (reason == \"backdrop click\") {\n $scope.reset($scope);\n }\n });\n}]);\n\nangular\n .module('nzbhydraApp')\n .factory('ConfigBoxService', ConfigBoxService);\n\nfunction ConfigBoxService($http, $q) {\n\n return {\n checkConnection: checkConnection,\n checkCaps: checkCaps\n };\n\n function checkConnection(url, settings) {\n var deferred = $q.defer();\n\n $http.post(url, settings).success(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \n if (result.result) {\n deferred.resolve();\n } else {\n deferred.reject({checked: true, message: result.message});\n }\n }).error(function (result) {\n deferred.reject({checked: false, message: result.message});\n });\n\n return deferred.promise;\n }\n\n function checkCaps(url, params, model) {\n var deferred = $q.defer();\n\n $http.post(url, params).success(function (data) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click \n if (data.success) {\n model.search_ids = data.supportedIds;\n model.searchTypes = data.supportedTypes;\n if (data.supportsAllCategories) { //Don't display all the categories, will be replaced with placeholder \"All categories\"\n model.categories = [];\n } else {\n model.categories = data.supportedCategories;\n }\n model.animeCategory = data.animeCategory;\n model.audiobookCategory = data.audiobookCategory;\n model.comicCategory = data.comicCategory;\n model.ebookCategory = data.ebookCategory;\n model.magazineCategory = data.magazineCategory;\n model.backend = data.backend;\n deferred.resolve({supportedIds: data.supportedIds, supportedTypes: data.supportedTypes}, model);\n } else {\n deferred.reject(data.message);\n }\n }).error(function () {\n deferred.reject(\"Unknown error\");\n });\n\n return deferred.promise;\n }\n\n}\nConfigBoxService.$inject = [\"$http\", \"$q\"];\n\n\n\n\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\n .module('nzbhydraApp')\n .factory('DownloaderCategoriesService', DownloaderCategoriesService);\n\nfunction DownloaderCategoriesService($http, $q, $uibModal) {\n\n var categories = {};\n var selectedCategory = {};\n\n var service = {\n get: getCategories,\n invalidate: invalidate,\n select: select,\n openCategorySelection: openCategorySelection\n };\n\n var deferred;\n\n return service;\n\n\n function getCategories(downloader) {\n\n function loadAll() {\n if (angular.isDefined(categories) && angular.isDefined(categories.downloader)) {\n var deferred = $q.defer();\n deferred.resolve(categories.downloader);\n return deferred.promise;\n }\n \n return $http.get('internalapi/getcategories', {params: {downloader: downloader.name}})\n .then(function (categoriesResponse) {\n \n console.log(\"Updating downloader categories cache\");\n var categories = {downloader: categoriesResponse.data.categories};\n return categoriesResponse.data.categories;\n\n }, function (error) {\n throw error;\n });\n }\n\n return loadAll().then(function (categories) {\n return categories;\n }, function (error) {\n throw error;\n });\n }\n\n\n function openCategorySelection(downloader) {\n $uibModal.open({\n templateUrl: 'static/html/directives/addable-nzb-modal.html',\n controller: 'DownloaderCategorySelectionController',\n size: \"sm\",\n resolve: {\n categories: function () {\n return getCategories(downloader)\n }\n }\n });\n deferred = $q.defer();\n return deferred.promise;\n }\n\n function select(category) {\n selectedCategory = category;\n console.log(\"Selected category \" + category);\n deferred.resolve(category);\n }\n\n function invalidate() {\n console.log(\"Invalidating categories\");\n categories = undefined;\n }\n}\nDownloaderCategoriesService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\n\nangular\n .module('nzbhydraApp').controller('DownloaderCategorySelectionController', [\"$scope\", \"$uibModalInstance\", \"DownloaderCategoriesService\", \"categories\", function ($scope, $uibModalInstance, DownloaderCategoriesService, categories) {\n console.log(categories);\n $scope.categories = categories;\n $scope.select = function (category) {\n DownloaderCategoriesService.select(category);\n $uibModalInstance.close($scope);\n }\n}]);","angular\n .module('nzbhydraApp')\n .controller('DownloadHistoryController', DownloadHistoryController);\n\n\nfunction DownloadHistoryController($scope, StatsService, downloads) {\n $scope.type = \"All\";\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n\n $scope.changeType = function (type) {\n $scope.type = type;\n getDownloadsPage($scope.pagination.current);\n };\n\n\n $scope.pageChanged = function (newPage) {\n getDownloadsPage(newPage);\n };\n\n function getDownloadsPage(pageNumber) {\n StatsService.getDownloadHistory(pageNumber, $scope.limit, $scope.type).then(function(downloads) {\n $scope.nzbDownloads = downloads.data.nzbDownloads;\n $scope.totalDownloads = downloads.data.totalDownloads;\n });\n \n }\n\n\n}\nDownloadHistoryController.$inject = [\"$scope\", \"StatsService\", \"downloads\"];\n","angular\n .module('nzbhydraApp')\n .factory('ConfigService', ConfigService);\n\nfunction ConfigService($http, $q, $cacheFactory) {\n\n var cache = $cacheFactory(\"nzbhydra\");\n\n return {\n set: set,\n get: get,\n getSafe: getSafe,\n invalidateSafe: invalidateSafe,\n maySeeAdminArea: maySeeAdminArea\n };\n\n\n function set(newConfig) {\n $http.put('internalapi/setsettings', newConfig)\n .then(function (successresponse) {\n console.log(\"Settings saved. Updating cache\");\n cache.put(\"config\", newConfig);\n invalidateSafe();\n }, function (errorresponse) {\n console.log(\"Error saving settings: \" + errorresponse);\n });\n }\n\n function get() {\n var config = cache.get(\"config\");\n if (angular.isUndefined(config)) {\n config = $http.get('internalapi/getconfig').then(function (data) {\n return data.data;\n });\n cache.put(\"config\", config);\n }\n\n return config;\n }\n\n function getSafe() {\n var safeconfig = cache.get(\"safeconfig\");\n if (angular.isDefined(safeconfig)) {\n return safeconfig;\n }\n \n return $http.get('internalapi/getsafeconfig').then(function (data) {\n cache.put(\"safeconfig\", data.data);\n return data.data;\n });\n\n\n }\n\n function invalidateSafe() {\n cache.remove(\"safeconfig\");\n }\n\n function maySeeAdminArea() {\n function loadAll() {\n var maySeeAdminArea = cache.get(\"maySeeAdminArea\");\n if (!angular.isUndefined(maySeeAdminArea)) {\n var deferred = $q.defer();\n deferred.resolve(maySeeAdminArea);\n return deferred.promise;\n }\n\n return $http.get('internalapi/mayseeadminarea')\n .then(function (configResponse) {\n var config = configResponse.data;\n cache.put(\"maySeeAdminArea\", config);\n return configResponse.data;\n });\n }\n\n return loadAll().then(function (maySeeAdminArea) {\n return maySeeAdminArea;\n });\n }\n}\nConfigService.$inject = [\"$http\", \"$q\", \"$cacheFactory\"];","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: '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 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: '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: '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: '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: getDownloaderPresets(),\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 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 },\r\n addNewText: 'Add new indexer',\r\n entryTemplateUrl: 'indexerEntry.html',\r\n presets: getIndexerPresets(),\r\n fieldsFunction: getIndexerBoxFields,\r\n allowDeleteFunction: function (model) {\r\n return model.type == 'newznab' || model.type == 'jackett';\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 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() {\r\n return [\r\n [\r\n {\r\n name: \"6box\",\r\n host: \"https://6box.me\"\r\n },\r\n {\r\n name: \"6box sptweb\",\r\n host: \"https://6box.me/spotweb\"\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://drunkenslug.com\"\r\n },\r\n {\r\n name: \"miatrix\",\r\n host: \"https://www.miatrix.com\"\r\n },\r\n {\r\n name: \"NZB Finder\",\r\n host: \"https://nzbfinder.ws\"\r\n },\r\n {\r\n name: \"NZBs.org\",\r\n host: \"https://nzbs.org\"\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: \"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: \"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: \"SimplyNZBs\",\r\n host: \"https://simplynzbs.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\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 fieldset.push(\r\n {\r\n key: 'hitLimitResetTime',\r\n type: 'horizontalInput',\r\n hideExpression: '!model.hitLimit',\r\n templateOptions: {\r\n type: 'number',\r\n label: 'API 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\r\n\r\n if (model.type != \"womble\") {\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 }\r\n if (model.type != \"womble\" && 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 != \"womble\" && 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 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.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\"];","angular\n .module('nzbhydraApp')\n .factory('ConfigModel', function () {\n return {};\n });\n\nangular\n .module('nzbhydraApp')\n .factory('ConfigWatcher', function () {\n var $scope;\n\n return {\n watch: watch\n };\n\n function watch(scope) {\n $scope = scope;\n $scope.$watchGroup([\"config.main.host\"], function () {\n }, true);\n }\n });\n\n\nangular\n .module('nzbhydraApp')\n .controller('ConfigController', ConfigController);\n\nfunction ConfigController($scope, $http, ConfigService, config, DownloaderCategoriesService, ConfigFields, ConfigModel, ModalService, RestartService, $state, growl, $rootScope) {\n $scope.config = config;\n $scope.submit = submit;\n $scope.activeTab = undefined;\n\n $scope.restartRequired = false;\n $scope.ignoreSaveNeeded = false;\n\n ConfigFields.setRestartWatcher(function () {\n $scope.restartRequired = true;\n });\n \n\n function submit() {\n if ($scope.form.$valid) {\n \n ConfigService.set($scope.config);\n ConfigService.invalidateSafe();\n $scope.form.$setPristine();\n DownloaderCategoriesService.invalidate();\n if ($scope.restartRequired) {\n ModalService.open(\"Restart required\", \"The changes you have made may require a restart to be effective.
Do you want to restart now?\", {\n yes: {\n onYes: function () {\n RestartService.restart();\n }\n },\n no: {\n onNo: function () {\n $scope.restartRequired = false;\n }\n }\n });\n }\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n \n //Ridiculously hacky way to make the error messages appear\n try {\n if (angular.isDefined(form.$error.required)) {\n _.each(form.$error.required, function (item) {\n if (angular.isDefined(item.$error.required)) {\n _.each(item.$error.required, function (item2) {\n item2.$setTouched();\n });\n } \n });\n }\n angular.forEach($scope.form.$error.required, function (field) {\n field.$setTouched();\n });\n } catch(err) {\n //\n }\n \n }\n }\n\n ConfigModel = config;\n\n $scope.fields = ConfigFields.getFields($scope.config);\n \n $scope.allTabs = [\n {\n active: false,\n state: 'root.config',\n name: 'Main',\n model: ConfigModel.main,\n fields: $scope.fields.main\n },\n {\n active: false,\n state: 'root.config.auth',\n name: 'Authorization',\n model: ConfigModel.auth,\n fields: $scope.fields.auth\n },\n {\n active: false,\n state: 'root.config.searching',\n name: 'Searching',\n model: ConfigModel.searching,\n fields: $scope.fields.searching\n },\n {\n active: false,\n state: 'root.config.categories',\n name: 'Categories',\n model: ConfigModel.categories,\n fields: $scope.fields.categories\n },\n {\n active: false,\n state: 'root.config.downloader',\n name: 'Downloaders',\n model: ConfigModel.downloaders,\n fields: $scope.fields.downloaders\n },\n {\n active: false,\n state: 'root.config.indexers',\n name: 'Indexers',\n model: ConfigModel.indexers,\n fields: $scope.fields.indexers\n }\n ];\n\n for (var i = 0; i < $scope.allTabs.length; i++) {\n if ($state.is($scope.allTabs[i].state)) {\n $scope.allTabs[i].active = true;\n $scope.activeTab = $scope.allTabs[i];\n }\n }\n\n $scope.isSavingNeeded = function () {\n return $scope.form.$dirty && $scope.form.$valid && !$scope.ignoreSaveNeeded;\n };\n\n $scope.goToConfigState = function (index) {\n $state.go($scope.allTabs[index].state);\n $scope.activeTab = $scope.allTabs[index]; \n };\n \n $scope.help = function() {\n $http.get(\"internalapi/gethelp\", {params: {id: $scope.activeTab.name}}).then(function(result) {\n var html = '' + result.data + \"\";\n ModalService.open($scope.activeTab.name + \" - Help\", html, {}, \"lg\");\n },\n function() {\n growl.error(\"Error while loading help\")\n })\n };\n\n $scope.$on('$stateChangeStart',\n function (event, toState, toParams, fromState, fromParams) {\n if ($scope.isSavingNeeded()) {\n event.preventDefault();\n ModalService.open(\"Unsaved changed\", \"Do you want to save before leaving?\", {\n yes: {\n onYes: function() {\n $scope.submit();\n $state.go(toState);\n },\n text: \"Yes\"\n },\n no: {\n onNo: function () {\n $scope.ignoreSaveNeeded = true;\n $scope.ctrl.options.resetModel();\n $state.go(toState);\n },\n text: \"No\"\n },\n cancel: {\n onCancel: function () {\n event.preventDefault();\n },\n text: \"Cancel\"\n }\n });\n } \n })\n}\nConfigController.$inject = [\"$scope\", \"$http\", \"ConfigService\", \"config\", \"DownloaderCategoriesService\", \"ConfigFields\", \"ConfigModel\", \"ModalService\", \"RestartService\", \"$state\", \"growl\", \"$rootScope\"];\n\n\n","angular\n .module('nzbhydraApp')\n .factory('CategoriesService', CategoriesService);\n\nfunction CategoriesService(ConfigService) {\n\n return {\n getByName: getByName,\n getAll: getAll,\n getDefault: getDefault\n };\n\n\n function getByName(name) {\n for (var category in ConfigService.getSafe().categories) {\n category = ConfigService.getSafe().categories[category];\n if (category.name == name || category.pretty == name) {\n return category;\n }\n }\n }\n \n function getAll() {\n return ConfigService.getSafe().categories;\n }\n \n function getDefault() {\n return getAll()[1];\n }\n\n}\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/version.txt b/version.txt index 841815c..6c094ce 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.175 \ No newline at end of file +0.2.176 \ No newline at end of file