Advertisement
Guest User

Untitled

a guest
Nov 21st, 2016
186
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 216.28 KB | None | 0 0
  1. 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']);
  2.  
  3. angular.module('nzbhydraApp').config(["$stateProvider", "$urlRouterProvider", "$locationProvider", "blockUIConfig", "$urlMatcherFactoryProvider", "$authProvider", "localStorageServiceProvider", "bootstrapped", function ($stateProvider, $urlRouterProvider, $locationProvider, blockUIConfig, $urlMatcherFactoryProvider, $authProvider, localStorageServiceProvider, bootstrapped) {
  4.  
  5. blockUIConfig.autoBlock = false;
  6. $urlMatcherFactoryProvider.strictMode(false);
  7.  
  8. $urlRouterProvider.otherwise("/");
  9.  
  10.  
  11. $stateProvider
  12. .state('root', {
  13. url: '',
  14. abstract: true,
  15. resolve: {
  16. //loginRequired: loginRequired
  17. },
  18. views: {
  19. 'header': {
  20. templateUrl: 'static/html/states/header.html',
  21. controller: 'HeaderController'
  22. },
  23. 'footer': {
  24. templateUrl: 'footer.html'
  25. }
  26. }
  27. })
  28. .state("root.config", {
  29. url: "/config",
  30. views: {
  31. 'container@': {
  32. templateUrl: "static/html/states/config.html",
  33. controller: "ConfigController",
  34. controllerAs: 'ctrl',
  35. resolve: {
  36. loginRequired: loginRequiredAdmin,
  37. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  38. return ConfigService.get();
  39. }],
  40. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  41. return ConfigService.getSafe();
  42. }],
  43. $title: ["$stateParams", function ($stateParams) {
  44. return "Config"
  45. }]
  46. }
  47. }
  48. }
  49. })
  50. .state("root.config.auth", {
  51. url: "/auth",
  52. views: {
  53. 'container@': {
  54. templateUrl: "static/html/states/config.html",
  55. controller: "ConfigController",
  56. resolve: {
  57. loginRequired: loginRequiredAdmin,
  58. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  59. return ConfigService.get();
  60. }],
  61. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  62. return ConfigService.getSafe();
  63. }],
  64. $title: ["$stateParams", function ($stateParams) {
  65. return "Config (Auth)"
  66. }]
  67. }
  68. }
  69. }
  70. })
  71. .state("root.config.searching", {
  72. url: "/searching",
  73. views: {
  74. 'container@': {
  75. templateUrl: "static/html/states/config.html",
  76. controller: "ConfigController",
  77. resolve: {
  78. loginRequired: loginRequiredAdmin,
  79. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  80. return ConfigService.get();
  81. }],
  82. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  83. return ConfigService.getSafe();
  84. }],
  85. $title: ["$stateParams", function ($stateParams) {
  86. return "Config (Searching)"
  87. }]
  88. }
  89. }
  90. }
  91. })
  92. .state("root.config.categories", {
  93. url: "/categories",
  94. views: {
  95. 'container@': {
  96. templateUrl: "static/html/states/config.html",
  97. controller: "ConfigController",
  98. resolve: {
  99. loginRequired: loginRequiredAdmin,
  100. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  101. return ConfigService.get();
  102. }],
  103. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  104. return ConfigService.getSafe();
  105. }],
  106. $title: ["$stateParams", function ($stateParams) {
  107. return "Config (Categories)"
  108. }]
  109. }
  110. }
  111. }
  112. })
  113. .state("root.config.downloader", {
  114. url: "/downloader",
  115. views: {
  116. 'container@': {
  117. templateUrl: "static/html/states/config.html",
  118. controller: "ConfigController",
  119. resolve: {
  120. loginRequired: loginRequiredAdmin,
  121. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  122. return ConfigService.get();
  123. }],
  124. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  125. return ConfigService.getSafe();
  126. }],
  127. $title: ["$stateParams", function ($stateParams) {
  128. return "Config (Downloader)"
  129. }]
  130. }
  131. }
  132. }
  133. })
  134. .state("root.config.indexers", {
  135. url: "/indexers",
  136. views: {
  137. 'container@': {
  138. templateUrl: "static/html/states/config.html",
  139. controller: "ConfigController",
  140. resolve: {
  141. loginRequired: loginRequiredAdmin,
  142. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  143. return ConfigService.get();
  144. }],
  145. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  146. return ConfigService.getSafe();
  147. }],
  148. $title: ["$stateParams", function ($stateParams) {
  149. return "Config (Indexers)"
  150. }]
  151. }
  152. }
  153. }
  154. })
  155. .state("root.config.system", {
  156. url: "/system",
  157. templateUrl: "static/html/states/config.html",
  158. views: {
  159. 'container@': {
  160. controller: "ConfigController",
  161. resolve: {
  162. loginRequired: loginRequiredAdmin,
  163. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  164. return ConfigService.get();
  165. }],
  166. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  167. return ConfigService.getSafe();
  168. }],
  169. $title: ["$stateParams", function ($stateParams) {
  170. return "System"
  171. }]
  172. }
  173. }
  174. }
  175. })
  176. .state("root.config.log", {
  177. url: "/log",
  178. views: {
  179. 'container@': {
  180. templateUrl: "static/html/states/config.html",
  181. controller: "ConfigController",
  182. resolve: {
  183. loginRequired: loginRequiredAdmin,
  184. config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  185. return ConfigService.get();
  186. }],
  187. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  188. return ConfigService.getSafe();
  189. }],
  190. $title: ["$stateParams", function ($stateParams) {
  191. return "System (Log)"
  192. }]
  193. }
  194. }
  195. }
  196. })
  197. .state("root.stats", {
  198. url: "/stats",
  199. abstract: true,
  200. views: {
  201. 'container@': {
  202. templateUrl: "static/html/states/stats.html",
  203. controller: ["$scope", "$state", function($scope, $state) {
  204. $scope.$state = $state;
  205. }],
  206. resolve: {
  207. loginRequired: loginRequiredStats,
  208. $title: ["$stateParams", function ($stateParams) {
  209. return "Stats"
  210. }]
  211. }
  212.  
  213. }
  214. }
  215. })
  216. .state("root.stats.main", {
  217. url: "/stats",
  218. views: {
  219. 'stats@root.stats': {
  220. templateUrl: "static/html/states/main-stats.html",
  221. controller: "StatsController",
  222. resolve: {
  223. loginRequired: loginRequiredStats,
  224. stats: ['loginRequired', 'StatsService', function (loginRequired, StatsService) {
  225. return StatsService.get();
  226. }],
  227. $title: ["$stateParams", function ($stateParams) {
  228. return "Stats"
  229. }]
  230. }
  231. }
  232. }
  233. })
  234. .state("root.stats.indexers", {
  235. url: "/indexers",
  236. views: {
  237. 'stats@root.stats': {
  238. templateUrl: "static/html/states/indexer-statuses.html",
  239. controller: IndexerStatusesController,
  240. resolve: {
  241. loginRequired: loginRequiredStats,
  242. statuses: ["$http", function($http) {
  243. return $http.get("internalapi/getindexerstatuses").success(function (response) {
  244. return response.indexerStatuses;
  245. });
  246. }],
  247. $title: ["$stateParams", function ($stateParams) {
  248. return "Stats (Indexers)"
  249. }]
  250. }
  251. }
  252. }
  253. })
  254. .state("root.stats.searches", {
  255. url: "/searches",
  256. views: {
  257. 'stats@root.stats': {
  258. templateUrl: "static/html/states/search-history.html",
  259. controller: SearchHistoryController,
  260. resolve: {
  261. loginRequired: loginRequiredStats,
  262. history: ["StatsService", function(StatsService) {
  263. return StatsService.getSearchHistory();
  264. }],
  265. $title: ["$stateParams", function ($stateParams) {
  266. return "Stats (Searches)"
  267. }]
  268. }
  269. }
  270. }
  271. })
  272. .state("root.stats.downloads", {
  273. url: "/downloads",
  274. views: {
  275. 'stats@root.stats': {
  276. templateUrl: 'static/html/states/download-history.html',
  277. controller: DownloadHistoryController,
  278. resolve: {
  279. loginRequired: loginRequiredStats,
  280. downloads: ["StatsService", function (StatsService) {
  281. return StatsService.getDownloadHistory();
  282. }],
  283. $title: ["$stateParams", function ($stateParams) {
  284. return "Stats (Downloads)"
  285. }]
  286. }
  287. }
  288. }
  289. })
  290. .state("root.system", {
  291. url: "/system",
  292. views: {
  293. 'container@': {
  294. templateUrl: "static/html/states/system.html",
  295. controller: "SystemController",
  296. resolve: {
  297. loginRequired: loginRequiredAdmin,
  298. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  299. return ConfigService.getSafe();
  300. }],
  301. askAdmin: ['loginRequired', '$http', function (loginRequired, $http) {
  302. return $http.get("internalapi/askadmin");
  303. }],
  304. $title: ["$stateParams", function ($stateParams) {
  305. return "System"
  306. }]
  307. }
  308. }
  309. }
  310. })
  311. .state("root.system.updates", {
  312. url: "/updates",
  313. views: {
  314. 'container@': {
  315. templateUrl: "static/html/states/system.html",
  316. controller: "SystemController",
  317. resolve: {
  318. loginRequired: loginRequiredAdmin,
  319. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  320. return ConfigService.getSafe();
  321. }],
  322. $title: ["$stateParams", function ($stateParams) {
  323. return "System (Updates)"
  324. }]
  325. }
  326. }
  327. }
  328. })
  329. .state("root.system.log", {
  330. url: "/log",
  331. views: {
  332. 'container@': {
  333. templateUrl: "static/html/states/system.html",
  334. controller: "SystemController",
  335. resolve: {
  336. loginRequired: loginRequiredAdmin,
  337. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  338. return ConfigService.getSafe();
  339. }],
  340. $title: ["$stateParams", function ($stateParams) {
  341. return "System (Log)"
  342. }]
  343. }
  344. }
  345. }
  346. })
  347. .state("root.system.backup", {
  348. url: "/backup",
  349. views: {
  350. 'container@': {
  351. templateUrl: "static/html/states/system.html",
  352. controller: "SystemController",
  353. resolve: {
  354. loginRequired: loginRequiredAdmin,
  355. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  356. return ConfigService.getSafe();
  357. }],
  358. $title: ["$stateParams", function ($stateParams) {
  359. return "System (Backup)"
  360. }]
  361. }
  362. }
  363. }
  364. })
  365. .state("root.system.about", {
  366. url: "/about",
  367. views: {
  368. 'container@': {
  369. templateUrl: "static/html/states/system.html",
  370. controller: "SystemController",
  371. resolve: {
  372. loginRequired: loginRequiredAdmin,
  373. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  374. return ConfigService.getSafe();
  375. }],
  376. $title: ["$stateParams", function ($stateParams) {
  377. return "System (About)"
  378. }]
  379. }
  380. }
  381. }
  382. })
  383. .state("root.system.bugreport", {
  384. url: "/bugreport",
  385. views: {
  386. 'container@': {
  387. templateUrl: "static/html/states/system.html",
  388. controller: "SystemController",
  389. resolve: {
  390. loginRequired: loginRequiredAdmin,
  391. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  392. return ConfigService.getSafe();
  393. }],
  394. $title: ["$stateParams", function ($stateParams) {
  395. return "System (Bug report)"
  396. }]
  397. }
  398. }
  399. }
  400. })
  401. .state("root.search", {
  402. url: "/?category&query&imdbid&tvdbid&title&season&episode&minsize&maxsize&minage&maxage&offsets&rid&mode&tmdbid&indexers",
  403. views: {
  404. 'container@': {
  405. templateUrl: "static/html/states/search.html",
  406. controller: "SearchController",
  407. resolve: {
  408. loginRequired: loginRequiredSearch,
  409. safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
  410. return ConfigService.getSafe();
  411. }],
  412. $title: ["$stateParams", function ($stateParams) {
  413. return "Search";
  414. }]
  415. }
  416. }
  417. }
  418. })
  419. .state("root.search.results", {
  420. views: {
  421. 'results@root.search': {
  422. templateUrl: "static/html/states/search-results.html",
  423. controller: "SearchResultsController",
  424. controllerAs: "srController",
  425. options: {
  426. inherit: true
  427. },
  428. resolve: {
  429. loginRequired: loginRequiredSearch,
  430. $title: ["$stateParams", function ($stateParams) {
  431. console.log($stateParams);
  432. var title = "Search results";
  433. var details;
  434. if ($stateParams.title) {
  435. details = $stateParams.title;
  436. } else if ($stateParams.query) {
  437. details = $stateParams.query;
  438. }
  439. if (details) {
  440. title += " (" + details + ")";
  441. }
  442. return title;
  443. }]
  444. }
  445. }
  446. }
  447. })
  448. .state("root.login", {
  449. url: "/login",
  450. views: {
  451. 'container@': {
  452. templateUrl: "static/html/states/login.html",
  453. controller: "LoginController",
  454. resolve: {
  455. loginRequired: function () {
  456. return null;
  457. },
  458. $title: ["$stateParams", function ($stateParams) {
  459. return "Login"
  460. }]
  461. }
  462. }
  463. }
  464. })
  465. ;
  466.  
  467.  
  468. $locationProvider.html5Mode(true);
  469.  
  470. $authProvider.httpInterceptor = function () {
  471. return true;
  472. };
  473. $authProvider.withCredentials = true;
  474. $authProvider.tokenRoot = null;
  475. $authProvider.baseUrl = bootstrapped.baseUrl;
  476. $authProvider.loginUrl = '/auth/login';
  477. $authProvider.signupUrl = '/auth/signup';
  478. $authProvider.unlinkUrl = '/unlink/';
  479. $authProvider.tokenName = 'token';
  480. $authProvider.tokenPrefix = 'satellizer';
  481. $authProvider.authHeader = 'TokenAuthorization';
  482. $authProvider.authToken = 'Bearer';
  483. $authProvider.storageType = 'localStorage';
  484.  
  485.  
  486. //Because I don't know for what state the login is required / asked I have a function for each
  487.  
  488. function loginRequiredSearch($q, $timeout, $auth, $state, bootstrapped) {
  489.  
  490. var deferred = $q.defer();
  491.  
  492. if (bootstrapped.authType != "form" || $auth.isAuthenticated() || !bootstrapped.searchRestricted) {
  493. deferred.resolve();
  494. } else {
  495. $timeout(function () {
  496. // This code runs after the authentication promise has been rejected.
  497. // Go to the log-in page
  498. $state.go("root.login");
  499. })
  500. }
  501. return deferred.promise;
  502. }
  503. loginRequiredSearch.$inject = ["$q", "$timeout", "$auth", "$state", "bootstrapped"];
  504.  
  505. function loginRequiredStats($q, $timeout, $auth, $state, bootstrapped) {
  506. var deferred = $q.defer();
  507.  
  508. if (bootstrapped.authType != "form" || $auth.isAuthenticated() || !bootstrapped.statsRestricted) {
  509. deferred.resolve();
  510. } else {
  511. $timeout(function () {
  512. // This code runs after the authentication promise has been rejected.
  513. // Go to the log-in page
  514. $state.go("root.login");
  515. })
  516. }
  517. return deferred.promise;
  518. }
  519. loginRequiredStats.$inject = ["$q", "$timeout", "$auth", "$state", "bootstrapped"];
  520.  
  521. function loginRequiredAdmin($q, $timeout, $auth, $state, bootstrapped) {
  522. var deferred = $q.defer();
  523.  
  524. if (bootstrapped.authType != "form" || $auth.isAuthenticated() || !bootstrapped.adminRestricted) {
  525. deferred.resolve();
  526. } else {
  527. $timeout(function () {
  528. // This code runs after the authentication promise has been rejected.
  529. // Go to the log-in page
  530. $state.go("root.login");
  531. })
  532. }
  533. return deferred.promise;
  534. }
  535. loginRequiredAdmin.$inject = ["$q", "$timeout", "$auth", "$state", "bootstrapped"];
  536.  
  537. localStorageServiceProvider
  538. .setPrefix('nzbhydra');
  539. localStorageServiceProvider
  540. .setNotify(true, false);
  541. }]);
  542.  
  543.  
  544. nzbhydraapp.config(["paginationTemplateProvider", function (paginationTemplateProvider) {
  545. paginationTemplateProvider.setPath('static/html/dirPagination.tpl.html');
  546. }]);
  547.  
  548. nzbhydraapp.config(['cfpLoadingBarProvider', function (cfpLoadingBarProvider) {
  549. cfpLoadingBarProvider.latencyThreshold = 100;
  550. }]);
  551.  
  552. nzbhydraapp.config(['growlProvider', function (growlProvider) {
  553. growlProvider.globalTimeToLive(5000);
  554. growlProvider.globalPosition('bottom-right');
  555. }]);
  556.  
  557. nzbhydraapp.directive('ngEnter', function () {
  558. return function (scope, element, attr) {
  559. element.bind("keydown keypress", function (event) {
  560. if (event.which === 13) {
  561. scope.$apply(function () {
  562. scope.$evalAsync(attr.ngEnter);
  563. });
  564.  
  565. event.preventDefault();
  566. }
  567. });
  568. };
  569. });
  570.  
  571. nzbhydraapp.filter('nzblink', function () {
  572. return function (resultItem) {
  573. var uri = new URI("internalapi/getnzb");
  574. uri.addQuery("searchResultId", resultItem.searchResultId);
  575. return uri.toString();
  576. }
  577. });
  578.  
  579. nzbhydraapp.factory('focus', ["$rootScope", "$timeout", function ($rootScope, $timeout) {
  580. return function (name) {
  581. $timeout(function () {
  582. $rootScope.$broadcast('focusOn', name);
  583. });
  584. }
  585. }]);
  586.  
  587. nzbhydraapp.run(["$rootScope", function ($rootScope) {
  588. $rootScope.$on('$stateChangeSuccess',
  589. function (event, toState, toParams, fromState, fromParams) {
  590. try {
  591. $rootScope.title = toState.views[Object.keys(toState.views)[0]].resolve.$title[1](toParams);
  592. } catch(e) {
  593.  
  594. }
  595.  
  596. });
  597. }]);
  598.  
  599.  
  600. nzbhydraapp.filter('unsafe', ["$sce", function ($sce) {
  601. return $sce.trustAsHtml;
  602. }]);
  603.  
  604. nzbhydraapp.filter('dereferer', ["ConfigService", function (ConfigService) {
  605. return function(url) {
  606. if (ConfigService.getSafe().dereferer) {
  607. return ConfigService.getSafe().dereferer.replace("$s", escape(url));
  608. }
  609. return url;
  610. }
  611. }]);
  612.  
  613. nzbhydraapp.config(["$provide", function ($provide) {
  614. $provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate, $injector) {
  615. return function (exception, cause) {
  616. $delegate(exception, cause);
  617. try {
  618. console.log(exception);
  619. var stack = exception.stack.split('\n').map(function (line) {
  620. return line.trim();
  621. });
  622. stack = stack.join("\n");
  623. //$injector.get("$http").put("internalapi/logerror", {error: stack, cause: angular.isDefined(cause) ? cause.toString() : "No known cause"});
  624.  
  625.  
  626. } catch (e) {
  627. console.error("Unable to log JS exception to server", e);
  628. }
  629. };
  630. }]);
  631. }]);
  632.  
  633. _.mixin({
  634. isNullOrEmpty: function (string) {
  635. return (_.isUndefined(string) || _.isNull(string) || (_.isString(string) && string.length === 0))
  636. }
  637. });
  638.  
  639. nzbhydraapp.factory('sessionInjector', ["$injector", function ($injector) {
  640. var sessionInjector = {
  641. response: function (response) {
  642. if (response.headers("Hydra-MaySeeAdmin") != null) {
  643. $injector.get("HydraAuthService").setLoggedInByBasic(response.headers("Hydra-MaySeeStats") == "True", response.headers("Hydra-MaySeeAdmin") == "True", response.headers("Hydra-Username"))
  644. }
  645.  
  646. return response;
  647. }
  648. };
  649. return sessionInjector;
  650. }]);
  651.  
  652. nzbhydraapp.config(['$httpProvider', function ($httpProvider) {
  653. $httpProvider.interceptors.push('sessionInjector');
  654. }]);
  655.  
  656. nzbhydraapp.directive('autoFocus', ["$timeout", function ($timeout) {
  657. return {
  658. restrict: 'AC',
  659. link: function (_scope, _element) {
  660. $timeout(function () {
  661. _element[0].focus();
  662. }, 0);
  663. }
  664. };
  665. }]);
  666. angular
  667. .module('nzbhydraApp')
  668. .directive('hydraupdates', hydraupdates);
  669.  
  670. function hydraupdates() {
  671. controller.$inject = ["$scope", "UpdateService", "$sce"];
  672. return {
  673. templateUrl: 'static/html/directives/updates.html',
  674. controller: controller
  675. };
  676.  
  677. function controller($scope, UpdateService, $sce) {
  678.  
  679. $scope.loadingPromise = UpdateService.getVersions().then(function (data) {
  680. $scope.currentVersion = data.data.currentVersion;
  681. $scope.repVersion = data.data.repVersion;
  682. $scope.updateAvailable = data.data.updateAvailable;
  683. $scope.changelog = data.data.changelog;
  684. });
  685.  
  686. UpdateService.getVersionHistory().then(function(data) {
  687. $scope.versionHistory = $sce.trustAsHtml(data.data.versionHistory);
  688. });
  689.  
  690. $scope.update = function () {
  691. UpdateService.update();
  692. };
  693.  
  694. $scope.showChangelog = function () {
  695. UpdateService.showChanges($scope.changelog);
  696. };
  697.  
  698.  
  699.  
  700. }
  701. }
  702.  
  703.  
  704. angular
  705. .module('nzbhydraApp')
  706. .directive('titleRow', titleRow);
  707.  
  708. function titleRow() {
  709. return {
  710. templateUrl: 'static/html/directives/title-row.html',
  711. scope: {
  712. duplicates: "<",
  713. selected: "<",
  714. rowIndex: "@"
  715. },
  716. controller: ['$scope', '$element', '$attrs', titleRowController]
  717. };
  718.  
  719. function titleRowController($scope) {
  720. $scope.expanded = false;
  721. console.log("Building title row");
  722. $scope.duplicatesToShow = duplicatesToShow;
  723. function duplicatesToShow() {
  724. if ($scope.expanded && $scope.duplicates.length > 1) {
  725. console.log("Showing all duplicates in group");
  726. return $scope.duplicates;
  727. } else {
  728. console.log("Showing first duplicate in group");
  729. return [$scope.duplicates[0]];
  730. }
  731. }
  732.  
  733. }
  734. }
  735. angular
  736. .module('nzbhydraApp')
  737. .directive('titleGroup', titleGroup);
  738.  
  739. function titleGroup() {
  740. return {
  741. templateUrl: 'static/html/directives/title-group.html',
  742. scope: {
  743. titles: "<",
  744. selected: "=",
  745. rowIndex: "<",
  746. doShowDuplicates: "<",
  747. internalRowIndex: "@"
  748. },
  749. controller: ['$scope', '$element', '$attrs', controller],
  750. multiElement: true
  751. };
  752.  
  753. function controller($scope, $element, $attrs) {
  754. $scope.expanded = false;
  755. $scope.titleGroupExpanded = false;
  756.  
  757. $scope.$on("toggleTitleExpansion", function (event, args) {
  758. $scope.titleGroupExpanded = args;
  759. event.stopPropagation();
  760. });
  761.  
  762.  
  763. $scope.titlesToShow = titlesToShow;
  764. function titlesToShow() {
  765. return $scope.titles.slice(1);
  766. }
  767.  
  768. }
  769. }
  770. angular
  771. .module('nzbhydraApp')
  772. .directive('tabOrChart', tabOrChart);
  773.  
  774. function tabOrChart() {
  775. return {
  776. templateUrl: 'static/html/directives/tab-or-chart.html',
  777. transclude: {
  778. "chartSlot": "chart",
  779. "tableSlot": "table"
  780. },
  781. restrict: 'E',
  782. replace: true,
  783. scope: {
  784. display: "@"
  785. }
  786.  
  787. };
  788.  
  789. }
  790.  
  791. angular
  792. .module('nzbhydraApp')
  793. .directive('searchResult', searchResult);
  794.  
  795. function searchResult() {
  796. return {
  797. templateUrl: 'static/html/directives/search-result.html',
  798. require: '^titleGroup',
  799. scope: {
  800. titleGroup: "<",
  801. showDuplicates: "<",
  802. selected: "<",
  803. rowIndex: "<"
  804. },
  805. controller: ['$scope', '$element', '$attrs', controller],
  806. multiElement: true
  807. };
  808.  
  809. function controller($scope, $element, $attrs) {
  810. $scope.titleGroupExpanded = false;
  811. $scope.hashGroupExpanded = {};
  812.  
  813. $scope.toggleTitleGroup = function () {
  814. $scope.titleGroupExpanded = !$scope.titleGroupExpanded;
  815. if (!$scope.titleGroupExpanded) {
  816. $scope.hashGroupExpanded[$scope.titleGroup[0][0].hash] = false; //Also collapse the first title's duplicates
  817. }
  818. };
  819.  
  820. $scope.groupingRowDuplicatesToShow = groupingRowDuplicatesToShow;
  821. function groupingRowDuplicatesToShow() {
  822. if ($scope.showDuplicates && $scope.titleGroup[0].length > 1 && $scope.hashGroupExpanded[$scope.titleGroup[0][0].hash]) {
  823. return $scope.titleGroup[0].slice(1);
  824. } else {
  825. return [];
  826. }
  827. }
  828.  
  829. //<div ng-repeat="hashGroup in titleGroup" ng-if="titleGroup.length > 0 && titleGroupExpanded" class="search-results-row">
  830. $scope.otherTitleRowsToShow = otherTitleRowsToShow;
  831. function otherTitleRowsToShow() {
  832. if ($scope.titleGroup.length > 1 && $scope.titleGroupExpanded) {
  833. return $scope.titleGroup.slice(1);
  834. } else {
  835. return [];
  836. }
  837. }
  838.  
  839. $scope.hashGroupDuplicatesToShow = hashGroupDuplicatesToShow;
  840. function hashGroupDuplicatesToShow(hashGroup) {
  841. if ($scope.showDuplicates && $scope.hashGroupExpanded[hashGroup[0].hash]) {
  842. return hashGroup.slice(1);
  843. } else {
  844. return [];
  845. }
  846. }
  847. }
  848. }
  849. angular
  850. .module('nzbhydraApp')
  851. .directive('otherColumns', otherColumns);
  852.  
  853. function otherColumns($http, $templateCache, $compile, $window) {
  854. controller.$inject = ["$scope", "$http", "$uibModal", "growl"];
  855. return {
  856. scope: {
  857. result: "<"
  858. },
  859. multiElement: true,
  860.  
  861. link: function (scope, element, attrs) {
  862. $http.get('static/html/directives/search-result-non-title-columns.html', {cache: $templateCache}).success(function (templateContent) {
  863. element.replaceWith($compile(templateContent)(scope));
  864. });
  865.  
  866. },
  867. controller: controller
  868. };
  869.  
  870. function controller($scope, $http, $uibModal, growl) {
  871.  
  872. $scope.showNfo = showNfo;
  873. function showNfo(resultItem) {
  874. if (resultItem.has_nfo == 0) {
  875. return;
  876. }
  877. var uri = new URI("internalapi/getnfo");
  878. uri.addQuery("searchresultid", resultItem.searchResultId);
  879. return $http.get(uri.toString()).then(function (response) {
  880. if (response.data.has_nfo) {
  881. $scope.openModal("lg", response.data.nfo)
  882. } else {
  883. if (!angular.isUndefined(resultItem.message)) {
  884. growl.error(resultItem.message);
  885. } else {
  886. growl.info("No NFO available");
  887. }
  888. }
  889. });
  890. }
  891.  
  892. $scope.openModal = openModal;
  893.  
  894. function openModal(size, nfo) {
  895. var modalInstance = $uibModal.open({
  896. template: '<pre class="nfo"><span ng-bind-html="nfo"></span></pre>',
  897. controller: 'NfoModalInstanceCtrl',
  898. size: size,
  899. resolve: {
  900. nfo: function () {
  901. return nfo;
  902. }
  903. }
  904. });
  905.  
  906. modalInstance.result.then();
  907. }
  908.  
  909. $scope.downloadNzb = downloadNzb;
  910.  
  911. function downloadNzb(resultItem) {
  912. //href = "{{ result.link }}"
  913. $window.location.href = resultItem.link;
  914. }
  915. }
  916. }
  917. otherColumns.$inject = ["$http", "$templateCache", "$compile", "$window"];
  918.  
  919. angular
  920. .module('nzbhydraApp')
  921. .controller('NfoModalInstanceCtrl', NfoModalInstanceCtrl);
  922.  
  923. function NfoModalInstanceCtrl($scope, $modalInstance, nfo) {
  924.  
  925. $scope.nfo = nfo;
  926.  
  927. $scope.ok = function () {
  928. $modalInstance.close($scope.selected.item);
  929. };
  930.  
  931. $scope.cancel = function () {
  932. $modalInstance.dismiss();
  933. };
  934. }
  935. NfoModalInstanceCtrl.$inject = ["$scope", "$modalInstance", "nfo"];
  936. //Can be used in an ng-repeat directive to call a function when the last element was rendered
  937. //We use it to mark the end of sorting / filtering so we can stop blocking the UI
  938.  
  939. angular
  940. .module('nzbhydraApp')
  941. .directive('onFinishRender', onFinishRender);
  942.  
  943. function onFinishRender($timeout) {
  944. function linkFunction(scope, element, attr) {
  945.  
  946. if (scope.$last === true) {
  947. $timeout(function () {
  948. scope.$evalAsync(attr.onFinishRender);
  949. });
  950. }
  951. }
  952.  
  953. return {
  954. link: linkFunction
  955. }
  956. }
  957. onFinishRender.$inject = ["$timeout"];
  958. angular
  959. .module('nzbhydraApp')
  960. .directive('hydralog', hydralog);
  961.  
  962. function hydralog() {
  963. controller.$inject = ["$scope", "$http", "$sce"];
  964. return {
  965. template: '<div cg-busy="{promise:logPromise,message:\'Loading log file\'}"><pre ng-bind-html="log" style="text-align: left; height: 65vh; overflow-y: scroll"></pre></div>',
  966. controller: controller
  967. };
  968.  
  969. function controller($scope, $http, $sce) {
  970. $scope.logPromise = $http.get("internalapi/getlogs").success(function (data) {
  971. $scope.log = $sce.trustAsHtml(data.log);
  972. });
  973.  
  974. }
  975. }
  976.  
  977.  
  978. angular
  979. .module('nzbhydraApp').directive("keepFocus", ['$timeout', function ($timeout) {
  980. /*
  981. Intended use:
  982. <input keep-focus ng-model='someModel.value'></input>
  983. */
  984. return {
  985. restrict: 'A',
  986. require: 'ngModel',
  987. link: function ($scope, $element, attrs, ngModel) {
  988.  
  989. ngModel.$parsers.unshift(function (value) {
  990. $timeout(function () {
  991. $element[0].focus();
  992. });
  993. return value;
  994. });
  995.  
  996. }
  997. };
  998. }])
  999. angular
  1000. .module('nzbhydraApp')
  1001. .directive('indexerInput', indexerInput);
  1002.  
  1003. function indexerInput() {
  1004. controller.$inject = ["$scope"];
  1005. return {
  1006. templateUrl: 'static/html/directives/indexer-input.html',
  1007. scope: {
  1008. indexer: "=",
  1009. model: "=",
  1010. onClick: "="
  1011. },
  1012. replace: true,
  1013. controller: controller
  1014. };
  1015.  
  1016. function controller($scope) {
  1017. $scope.isFocused = false;
  1018.  
  1019. $scope.onFocus = function() {
  1020. $scope.isFocused = true;
  1021. };
  1022.  
  1023. $scope.onBlur = function () {
  1024. $scope.isFocused = false;
  1025. };
  1026.  
  1027. }
  1028. }
  1029.  
  1030.  
  1031. angular
  1032. .module('nzbhydraApp').directive('focusOn', focusOn);
  1033.  
  1034. function focusOn() {
  1035. return directive;
  1036. function directive(scope, elem, attr) {
  1037. scope.$on('focusOn', function (e, name) {
  1038. if (name === attr.focusOn) {
  1039. elem[0].focus();
  1040. }
  1041. });
  1042. }
  1043. }
  1044.  
  1045. angular
  1046. .module('nzbhydraApp')
  1047. .directive('duplicateGroup', duplicateGroup);
  1048.  
  1049. function duplicateGroup() {
  1050. titleRowController.$inject = ["$scope", "localStorageService"];
  1051. return {
  1052. templateUrl: 'static/html/directives/duplicate-group.html',
  1053. scope: {
  1054. duplicates: "<",
  1055. selected: "=",
  1056. isFirstRow: "<",
  1057. rowIndex: "<",
  1058. displayTitleToggle: "<",
  1059. internalRowIndex: "@"
  1060. },
  1061. controller: titleRowController
  1062. };
  1063.  
  1064. function titleRowController($scope, localStorageService) {
  1065. $scope.internalRowIndex = Number($scope.internalRowIndex);
  1066. $scope.rowIndex = Number($scope.rowIndex);
  1067. $scope.titlesExpanded = false;
  1068. $scope.duplicatesExpanded = false;
  1069. $scope.foo = {
  1070. duplicatesDisplayed: localStorageService.get("duplicatesDisplayed") != null ? localStorageService.get("duplicatesDisplayed") : false
  1071. };
  1072. $scope.duplicatesToShow = duplicatesToShow;
  1073. function duplicatesToShow() {
  1074. return $scope.duplicates.slice(1);
  1075. }
  1076.  
  1077. $scope.toggleTitleExpansion = function () {
  1078. $scope.titlesExpanded = !$scope.titlesExpanded;
  1079. $scope.$emit("toggleTitleExpansion", $scope.titlesExpanded);
  1080. };
  1081.  
  1082. $scope.toggleDuplicateExpansion = function () {
  1083. $scope.duplicatesExpanded = !$scope.duplicatesExpanded;
  1084. };
  1085.  
  1086. $scope.$on("invertSelection", function () {
  1087. for (var i = 0; i < $scope.duplicates.length; i++) {
  1088. if ($scope.duplicatesExpanded) {
  1089. invertSelection($scope.selected, $scope.duplicates[i]);
  1090. } else {
  1091. if (i > 0) {
  1092. //Always remove duplicates that aren't displayed
  1093. invertSelection($scope.selected, $scope.duplicates[i], true);
  1094. } else {
  1095. invertSelection($scope.selected, $scope.duplicates[i]);
  1096. }
  1097. }
  1098. }
  1099. });
  1100.  
  1101. $scope.$on("duplicatesDisplayed", function (event, args) {
  1102. $scope.foo.duplicatesDisplayed = args;
  1103. });
  1104.  
  1105. $scope.clickCheckbox = function (event) {
  1106. var globalCheckboxIndex = $scope.rowIndex * 1000 + $scope.internalRowIndex * 100 + Number(event.currentTarget.dataset.checkboxIndex);
  1107. console.log(globalCheckboxIndex);
  1108. $scope.$emit("checkboxClicked", event, globalCheckboxIndex, event.currentTarget.checked);
  1109. };
  1110.  
  1111. function isBetween(num, betweena, betweenb) {
  1112. return (betweena <= num && num <= betweenb) || (betweena >= num && num >= betweenb);
  1113. }
  1114.  
  1115. $scope.$on("shiftClick", function (event, startIndex, endIndex, newValue) {
  1116. var globalDuplicateGroupIndex = $scope.rowIndex * 1000 + $scope.internalRowIndex * 100;
  1117. if (isBetween(globalDuplicateGroupIndex, startIndex, endIndex)) {
  1118.  
  1119. for (var i = 0; i < $scope.duplicates.length; i++) {
  1120. if (isBetween(globalDuplicateGroupIndex + i, startIndex, endIndex)) {
  1121. if (i == 0 || $scope.duplicatesExpanded) {
  1122. console.log("Indirectly clicked row with global index " + (globalDuplicateGroupIndex + i) + " setting new checkbox value to " + newValue);
  1123. var index = _.indexOf($scope.selected, $scope.duplicates[i]);
  1124. if (index == -1 && newValue) {
  1125. console.log("Adding to selection");
  1126. $scope.selected.push($scope.duplicates[i]);
  1127. } else if (index > -1 && !newValue) {
  1128. $scope.selected.splice(index, 1);
  1129. console.log("Removing from selection");
  1130. }
  1131. }
  1132. }
  1133. }
  1134. }
  1135. });
  1136.  
  1137. function invertSelection(a, b, dontPush) {
  1138. var index = _.indexOf(a, b);
  1139. if (index > -1) {
  1140. a.splice(index, 1);
  1141. } else {
  1142. if (!dontPush)
  1143. a.push(b);
  1144. }
  1145. }
  1146. }
  1147.  
  1148.  
  1149. }
  1150. angular
  1151. .module('nzbhydraApp')
  1152. .directive('downloadNzbsButton', downloadNzbsButton);
  1153.  
  1154. function downloadNzbsButton() {
  1155. controller.$inject = ["$scope", "NzbDownloadService", "growl"];
  1156. return {
  1157. templateUrl: 'static/html/directives/download-nzbs-button.html',
  1158. require: ['^searchResults'],
  1159. scope: {
  1160. searchResults: "<"
  1161. },
  1162. controller: controller
  1163. };
  1164.  
  1165. function controller($scope, NzbDownloadService, growl) {
  1166.  
  1167. $scope.downloaders = NzbDownloadService.getEnabledDownloaders();
  1168.  
  1169. $scope.download = function (downloader) {
  1170. if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length == 0) {
  1171. growl.info("You should select at least one result...");
  1172. } else {
  1173.  
  1174. var values = _.map($scope.searchResults, function (value) {
  1175. return value.searchResultId;
  1176. });
  1177.  
  1178. NzbDownloadService.download(downloader, values).then(function (response) {
  1179. if (response.data.success) {
  1180. growl.info("Successfully added " + response.data.added + " of " + response.data.of + " NZBs");
  1181. } else {
  1182. growl.error("Error while adding NZBs");
  1183. }
  1184. }, function () {
  1185. growl.error("Error while adding NZBs");
  1186. });
  1187. }
  1188. }
  1189.  
  1190.  
  1191. }
  1192. }
  1193.  
  1194.  
  1195. angular
  1196. .module('nzbhydraApp')
  1197. .directive('connectionTest', connectionTest);
  1198.  
  1199. function connectionTest() {
  1200. controller.$inject = ["$scope"];
  1201. return {
  1202. templateUrl: 'static/html/directives/connection-test.html',
  1203. require: ['^type', '^data'],
  1204. scope: {
  1205. type: "=",
  1206. id: "=",
  1207. data: "=",
  1208. downloader: "="
  1209. },
  1210. controller: controller
  1211. };
  1212.  
  1213. function controller($scope) {
  1214. $scope.message = "";
  1215. console.log($scope);
  1216.  
  1217. var testButton = "#button-test-connection";
  1218. var testMessage = "#message-test-connection";
  1219.  
  1220. function showSuccess() {
  1221. angular.element(testButton).removeClass("btn-default");
  1222. angular.element(testButton).removeClass("btn-danger");
  1223. angular.element(testButton).addClass("btn-success");
  1224. }
  1225.  
  1226. function showError() {
  1227. angular.element(testButton).removeClass("btn-default");
  1228. angular.element(testButton).removeClass("btn-success");
  1229. angular.element(testButton).addClass("btn-danger");
  1230. }
  1231.  
  1232. $scope.testConnection = function () {
  1233. angular.element(testButton).addClass("glyphicon-refresh-animate");
  1234. var myInjector = angular.injector(["ng"]);
  1235. var $http = myInjector.get("$http");
  1236. var url;
  1237. var params;
  1238. if ($scope.type == "downloader") {
  1239. url = "internalapi/test_downloader";
  1240. params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};
  1241. if ($scope.downloader == "sabnzbd") {
  1242. params.apikey = $scope.data.apikey;
  1243. params.url = $scope.data.url;
  1244. } else {
  1245. params.host = $scope.data.host;
  1246. params.port = $scope.data.port;
  1247. params.ssl = $scope.data.ssl;
  1248. }
  1249. } else if ($scope.data.type == "newznab") {
  1250. url = "internalapi/test_newznab";
  1251. params = {host: $scope.data.host, apikey: $scope.data.apikey};
  1252. }
  1253. $http.get(url, {params: params}).success(function (result) {
  1254. //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click
  1255. if (result.result) {
  1256. angular.element(testMessage).text("");
  1257. showSuccess();
  1258. } else {
  1259. angular.element(testMessage).text(result.message);
  1260. showError();
  1261. }
  1262.  
  1263. }).error(function () {
  1264. angular.element(testMessage).text(result.message);
  1265. showError();
  1266. }).finally(function () {
  1267. angular.element(testButton).removeClass("glyphicon-refresh-animate");
  1268. })
  1269. }
  1270.  
  1271. }
  1272. }
  1273.  
  1274.  
  1275. angular
  1276. .module('nzbhydraApp')
  1277. .directive('cfgFormEntry', cfgFormEntry);
  1278.  
  1279. function cfgFormEntry() {
  1280. return {
  1281. templateUrl: 'static/html/directives/cfg-form-entry.html',
  1282. require: ["^title", "^cfg"],
  1283. scope: {
  1284. title: "@",
  1285. cfg: "=",
  1286. help: "@",
  1287. type: "@?",
  1288. options: "=?"
  1289. },
  1290. controller: ["$scope", "$element", "$attrs", function ($scope, $element, $attrs) {
  1291. $scope.type = angular.isDefined($scope.type) ? $scope.type : 'text';
  1292. $scope.options = angular.isDefined($scope.type) ? $scope.$eval($attrs.options) : [];
  1293. }]
  1294. };
  1295. }
  1296. angular
  1297. .module('nzbhydraApp')
  1298. .directive('hydrabackup', hydrabackup);
  1299.  
  1300. function hydrabackup() {
  1301. controller.$inject = ["$scope", "BackupService", "Upload", "RequestsErrorHandler", "growl", "RestartService", "$http"];
  1302. return {
  1303. templateUrl: 'static/html/directives/backup.html',
  1304. controller: controller
  1305. };
  1306.  
  1307. function controller($scope, BackupService, Upload, RequestsErrorHandler, growl, RestartService, $http) {
  1308. $scope.refreshBackupList = function () {
  1309. BackupService.getBackupsList().then(function (backups) {
  1310. $scope.backups = backups;
  1311. });
  1312. };
  1313.  
  1314. $scope.refreshBackupList();
  1315.  
  1316. $scope.uploadActive = false;
  1317.  
  1318.  
  1319. $scope.createAndDownloadBackupFile = function() {
  1320.  
  1321. $http({method: 'GET', url: 'internalapi/getbackup', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {
  1322. var a = document.createElement('a');
  1323. var blob = new Blob([data], {'type': "application/octet-stream"});
  1324. a.href = URL.createObjectURL(blob);
  1325. a.download = "nzbhydra-backup-" + moment().format("YYYY-MM-DD-HH-mm") + ".zip";
  1326.  
  1327. document.body.appendChild(a);
  1328. a.click();
  1329. document.body.removeChild(a);
  1330. $scope.refreshBackupList();
  1331. }).error(function (data, status, headers, config) {
  1332. console.log("Error:" + status);
  1333. });
  1334.  
  1335. };
  1336.  
  1337. $scope.uploadBackupFile = function (file, errFiles) {
  1338. RequestsErrorHandler.specificallyHandled(function () {
  1339. console.log("Hallo");
  1340. $scope.file = file;
  1341. $scope.errFile = errFiles && errFiles[0];
  1342. if (file) {
  1343. $scope.uploadActive = true;
  1344. file.upload = Upload.upload({
  1345. url: 'internalapi/restorebackup',
  1346. data: {content: file}
  1347. });
  1348.  
  1349. file.upload.then(function (response) {
  1350. $scope.uploadActive = false;
  1351. file.result = response.data;
  1352. RestartService.restart("Restore successful.");
  1353.  
  1354. }, function (response) {
  1355. $scope.uploadActive = false;
  1356. growl.error(response.data)
  1357. }, function (evt) {
  1358. file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));
  1359. file.loaded = Math.floor(evt.loaded / 1024);
  1360. file.total = Math.floor(evt.total / 1024);
  1361. });
  1362. }
  1363. });
  1364. };
  1365.  
  1366. $scope.restoreFromFile = function(filename) {
  1367. BackupService.restoreFromFile(filename).then(function() {
  1368. RestartService.restart("Restore successful.");
  1369. },
  1370. function(response) {
  1371. growl.error(response.data);
  1372. })
  1373. }
  1374.  
  1375. }
  1376. }
  1377.  
  1378.  
  1379. angular
  1380. .module('nzbhydraApp')
  1381. .directive('addableNzbs', addableNzbs);
  1382.  
  1383. function addableNzbs() {
  1384. controller.$inject = ["$scope", "NzbDownloadService"];
  1385. return {
  1386. templateUrl: 'static/html/directives/addable-nzbs.html',
  1387. require: ['^searchResultId'],
  1388. scope: {
  1389. searchResultId: "<",
  1390. downloadType: "<"
  1391. },
  1392. controller: controller
  1393. };
  1394.  
  1395. function controller($scope, NzbDownloadService) {
  1396. $scope.downloaders = _.filter(NzbDownloadService.getEnabledDownloaders(), function(downloader) {
  1397. if ($scope.downloadType != "nzb") {
  1398. return downloader.downloadType == $scope.downloadType
  1399. }
  1400. return true;
  1401. });
  1402. }
  1403. }
  1404.  
  1405. angular
  1406. .module('nzbhydraApp')
  1407. .directive('addableNzb', addableNzb);
  1408.  
  1409. function addableNzb() {
  1410. controller.$inject = ["$scope", "NzbDownloadService", "growl"];
  1411. return {
  1412. templateUrl: 'static/html/directives/addable-nzb.html',
  1413. scope: {
  1414. searchResultId: "<",
  1415. downloader: "<"
  1416. },
  1417. controller: controller
  1418. };
  1419.  
  1420. function controller($scope, NzbDownloadService, growl) {
  1421. if ($scope.downloader.iconCssClass) {
  1422. $scope.cssClass = "fa fa-" + $scope.downloader.iconCssClass.replace("fa-","").replace("fa ", "");
  1423. } else {
  1424. $scope.cssClass = $scope.downloader.type == "sabnzbd" ? "sabnzbd" : "nzbget";
  1425. }
  1426.  
  1427. $scope.add = function () {
  1428. $scope.cssClass = "nzb-spinning";
  1429. NzbDownloadService.download($scope.downloader, [$scope.searchResultId]).then(function (response) {
  1430. if (response.data.success) {
  1431. $scope.cssClass = $scope.downloader.type == "sabnzbd" ? "sabnzbd-success" : "nzbget-success";
  1432. } else {
  1433. $scope.cssClass = $scope.downloader.type == "sabnzbd" ? "sabnzbd-error" : "nzbget-error";
  1434. growl.error("Unable to add NZB. Make sure the downloader is running and properly configured.");
  1435. }
  1436. }, function () {
  1437. $scope.cssClass = $scope.downloader.type == "sabnzbd" ? "sabnzbd-error" : "nzbget-error";
  1438. growl.error("An unexpected error occurred while trying to contact NZB Hydra or add the NZB.");
  1439. })
  1440. };
  1441.  
  1442.  
  1443.  
  1444. }
  1445. }
  1446.  
  1447. angular
  1448. .module('nzbhydraApp')
  1449. .factory('UpdateService', UpdateService);
  1450.  
  1451. function UpdateService($http, growl, blockUI, RestartService) {
  1452.  
  1453. var currentVersion;
  1454. var repVersion;
  1455. var updateAvailable;
  1456. var changelog;
  1457. var versionHistory;
  1458.  
  1459. return {
  1460. update: update,
  1461. showChanges: showChanges,
  1462. getVersions: getVersions,
  1463. getChangelog: getChangelog,
  1464. getVersionHistory: getVersionHistory
  1465. };
  1466.  
  1467.  
  1468.  
  1469. function getVersions() {
  1470. return $http.get("internalapi/get_versions").then(function (data) {
  1471. currentVersion = data.data.currentVersion;
  1472. repVersion = data.data.repVersion;
  1473. updateAvailable = data.data.updateAvailable;
  1474. return data;
  1475. });
  1476. }
  1477.  
  1478. function getChangelog() {
  1479. return $http.get("internalapi/get_changelog", {currentVersion: currentVersion, repVersion: repVersion}).then(function (data) {
  1480. changelog = data.data.changelog;
  1481. return data;
  1482. });
  1483. }
  1484.  
  1485. function getVersionHistory() {
  1486. return $http.get("internalapi/get_version_history").then(function (data) {
  1487. versionHistory = data.data.versionHistory;
  1488. return data;
  1489. });
  1490. }
  1491.  
  1492. function showChanges(changelog) {
  1493.  
  1494. var myInjector = angular.injector(["ng", "ui.bootstrap"]);
  1495. var $uibModal = myInjector.get("$uibModal");
  1496. var params = {
  1497. size: "lg",
  1498. templateUrl: "static/html/changelog.html",
  1499. resolve: {
  1500. changelog: function () {
  1501. return changelog;
  1502. }
  1503. },
  1504. controller: function ($scope, $sce, $uibModalInstance, changelog) {
  1505. //I fucking hate that untrusted HTML shit
  1506. changelog = $sce.trustAsHtml(changelog);
  1507. $scope.changelog = changelog;
  1508. console.log(changelog);
  1509. $scope.ok = function () {
  1510. $uibModalInstance.dismiss();
  1511. };
  1512. }
  1513. };
  1514.  
  1515. var modalInstance = $uibModal.open(params);
  1516.  
  1517. modalInstance.result.then();
  1518. }
  1519.  
  1520.  
  1521. function update() {
  1522. blockUI.start("Updating. Please stand by...");
  1523. $http.get("internalapi/update").then(function (data) {
  1524. if (data.data.success) {
  1525. RestartService.restart("Update complete.", 15);
  1526. } else {
  1527. blockUI.reset();
  1528. growl.info("An error occurred while updating. Please check the logs.");
  1529. }
  1530. },
  1531. function () {
  1532. blockUI.reset();
  1533. growl.info("An error occurred while updating. Please check the logs.");
  1534. });
  1535. }
  1536. }
  1537. UpdateService.$inject = ["$http", "growl", "blockUI", "RestartService"];
  1538.  
  1539.  
  1540. angular
  1541. .module('nzbhydraApp')
  1542. .controller('UpdateFooterController', UpdateFooterController);
  1543.  
  1544. function UpdateFooterController($scope, UpdateService, HydraAuthService, bootstrapped) {
  1545.  
  1546. $scope.updateAvailable = false;
  1547. $scope.checked = false;
  1548.  
  1549. $scope.mayUpdate = HydraAuthService.getUserRights().maySeeAdmin || bootstrapped.maySeeAdmin;
  1550.  
  1551. $scope.$on("user:loggedIn", function (event, data) {
  1552. if (data.maySeeAdmin && !$scope.checked) {
  1553. retrieveUpdateInfos();
  1554. }
  1555. });
  1556.  
  1557.  
  1558. if ($scope.mayUpdate) {
  1559. retrieveUpdateInfos();
  1560. }
  1561.  
  1562. function retrieveUpdateInfos() {
  1563. $scope.checked = true;
  1564. UpdateService.getVersions().then(function (data) {
  1565. $scope.currentVersion = data.data.currentVersion;
  1566. $scope.repVersion = data.data.repVersion;
  1567. $scope.updateAvailable = data.data.updateAvailable;
  1568. $scope.changelog = data.data.changelog;
  1569. });
  1570. }
  1571.  
  1572.  
  1573. $scope.update = function () {
  1574. UpdateService.update();
  1575. };
  1576.  
  1577. $scope.showChangelog = function () {
  1578. UpdateService.showChanges($scope.changelog);
  1579. }
  1580.  
  1581. }
  1582. UpdateFooterController.$inject = ["$scope", "UpdateService", "HydraAuthService", "bootstrapped"];
  1583.  
  1584. angular
  1585. .module('nzbhydraApp')
  1586. .controller('SystemController', SystemController);
  1587.  
  1588. function SystemController($scope, $state, $http, growl, RestartService, NzbHydraControlService) {
  1589.  
  1590.  
  1591. $scope.shutdown = function () {
  1592. NzbHydraControlService.shutdown().then(function () {
  1593. growl.info("Shutdown initiated. Cya!");
  1594. },
  1595. function () {
  1596. growl.info("Unable to send shutdown command.");
  1597. })
  1598. };
  1599.  
  1600. $scope.restart = function () {
  1601. RestartService.restart();
  1602. };
  1603.  
  1604.  
  1605. $scope.tabs = [
  1606. {
  1607. active: false,
  1608. state: 'root.system'
  1609. },
  1610. {
  1611. active: false,
  1612. state: 'root.system.updates'
  1613. },
  1614. {
  1615. active: false,
  1616. state: 'root.system.log'
  1617. },
  1618. {
  1619. active: false,
  1620. state: 'root.system.backup'
  1621. },
  1622. {
  1623. active: false,
  1624. state: 'root.system.bugreport'
  1625. },
  1626. {
  1627. active: false,
  1628. state: 'root.system.about'
  1629. }
  1630. ];
  1631.  
  1632.  
  1633. for (var i = 0; i < $scope.tabs.length; i++) {
  1634. if ($state.is($scope.tabs[i].state)) {
  1635. $scope.tabs[i].active = true;
  1636. }
  1637. }
  1638.  
  1639.  
  1640. $scope.goToState = function (index) {
  1641. $state.go($scope.tabs[index].state);
  1642.  
  1643. };
  1644.  
  1645. $scope.downloadDebuggingInfos = function() {
  1646. $http({method: 'GET', url: 'internalapi/getdebugginginfos', responseType: 'arraybuffer'}).success(function (data, status, headers, config) {
  1647. var a = document.createElement('a');
  1648. var blob = new Blob([data], {'type': "application/octet-stream"});
  1649. a.href = URL.createObjectURL(blob);
  1650. var filename = "nzbhydra-debuginfo-" + moment().format("YYYY-MM-DD-HH-mm") + ".zip";
  1651. a.download = filename;
  1652.  
  1653. document.body.appendChild(a);
  1654. a.click();
  1655. document.body.removeChild(a);
  1656. }).error(function (data, status, headers, config) {
  1657. console.log("Error:" + status);
  1658. });
  1659. }
  1660.  
  1661. }
  1662. SystemController.$inject = ["$scope", "$state", "$http", "growl", "RestartService", "NzbHydraControlService"];
  1663.  
  1664. angular
  1665. .module('nzbhydraApp')
  1666. .factory('StatsService', StatsService);
  1667.  
  1668. function StatsService($http) {
  1669.  
  1670. return {
  1671. get: getStats,
  1672. getSearchHistory: getSearchHistory,
  1673. getDownloadHistory: getDownloadHistory
  1674. };
  1675.  
  1676. function getStats() {
  1677. return $http.get("internalapi/getstats").success(function (response) {
  1678. return response.data;
  1679. });
  1680. }
  1681.  
  1682. function getSearchHistory(pageNumber, limit, type) {
  1683. if (angular.isUndefined(pageNumber)) {
  1684. pageNumber = 1;
  1685. }
  1686. if (angular.isUndefined(limit)) {
  1687. limit = 100;
  1688. }
  1689. if (angular.isUndefined(type)) {
  1690. type = "All";
  1691. }
  1692. return $http.get("internalapi/getsearchrequests", {params: {page: pageNumber, limit: limit, type: type}}).success(function (response) {
  1693. return {
  1694. searchRequests: response.searchRequests,
  1695. totalRequests: response.totalRequests
  1696. }
  1697. });
  1698. }
  1699.  
  1700. function getDownloadHistory(pageNumber, limit, type) {
  1701. if (angular.isUndefined(pageNumber)) {
  1702. pageNumber = 1;
  1703. }
  1704. if (angular.isUndefined(limit)) {
  1705. limit = 100;
  1706. }
  1707. if (angular.isUndefined(type)) {
  1708. type = "All";
  1709. }
  1710. console.log(1);
  1711. return $http.get("internalapi/getnzbdownloads", {params: {page: pageNumber, limit: limit, type: type}}).success(function (response) {
  1712. console.log(2);
  1713. return {
  1714. nzbDownloads: response.nzbDownloads,
  1715. totalDownloads: response.totalDownloads
  1716. };
  1717.  
  1718. });
  1719. }
  1720.  
  1721. }
  1722. StatsService.$inject = ["$http"];
  1723. angular
  1724. .module('nzbhydraApp')
  1725. .controller('StatsController', StatsController);
  1726.  
  1727. function StatsController($scope, $filter, stats) {
  1728.  
  1729. stats = stats.data;
  1730. $scope.nzbDownloads = null;
  1731. $scope.avgResponseTimes = stats.avgResponseTimes;
  1732. $scope.avgIndexerSearchResultsShares = stats.avgIndexerSearchResultsShares;
  1733. $scope.avgIndexerAccessSuccesses = stats.avgIndexerAccessSuccesses;
  1734. $scope.indexerDownloadShares = stats.indexerDownloadShares;
  1735. $scope.downloadsPerHourOfDay = stats.timeBasedDownloadStats.perHourOfDay;
  1736. $scope.downloadsPerDayOfWeek = stats.timeBasedDownloadStats.perDayOfWeek;
  1737. $scope.searchesPerHourOfDay = stats.timeBasedSearchStats.perHourOfDay;
  1738. $scope.searchesPerDayOfWeek = stats.timeBasedSearchStats.perDayOfWeek;
  1739.  
  1740. function getChart(chartType, values, xKey, yKey, xAxisLabel, yAxisLabel) {
  1741. return {
  1742. options: {
  1743. chart: {
  1744. type: chartType,
  1745. height: 350,
  1746. margin: {
  1747. top: 20,
  1748. right: 20,
  1749. bottom: 100,
  1750. left: 50
  1751. },
  1752. x: function (d) {
  1753. return d[xKey];
  1754. },
  1755. y: function (d) {
  1756. return d[yKey];
  1757. },
  1758. showValues: true,
  1759. valueFormat: function (d) {
  1760. return d;
  1761. },
  1762. color: function () {
  1763. return "red"
  1764. },
  1765. showControls: false,
  1766. showLegend: false,
  1767. duration: 100,
  1768. xAxis: {
  1769. axisLabel: xAxisLabel,
  1770. tickFormat: function (d) {
  1771. return d;
  1772. },
  1773. rotateLabels: 30,
  1774. showMaxMin: false,
  1775. color: function () {
  1776. return "white"
  1777. }
  1778. },
  1779. yAxis: {
  1780. axisLabel: yAxisLabel,
  1781. axisLabelDistance: -10,
  1782. tickFormat: function (d) {
  1783. return d;
  1784. }
  1785. },
  1786. tooltip: {
  1787. enabled: false
  1788. },
  1789. zoom: {
  1790. enabled: true,
  1791. scaleExtent: [1, 10],
  1792. useFixedDomain: false,
  1793. useNiceScale: false,
  1794. horizontalOff: false,
  1795. verticalOff: true,
  1796. unzoomEventType: 'dblclick.zoom'
  1797. }
  1798. }
  1799. }, data: [{
  1800. "key": "doesntmatter",
  1801. "bar": true,
  1802. "values": values
  1803. }]
  1804. };
  1805. }
  1806.  
  1807. $scope.avgResponseTimesChart = getChart("multiBarHorizontalChart", $scope.avgResponseTimes, "name", "avgResponseTime", "", "Response time");
  1808. $scope.avgResponseTimesChart.options.chart.margin.left = 100;
  1809. $scope.avgResponseTimesChart.options.chart.yAxis.rotateLabels = -30;
  1810.  
  1811.  
  1812. $scope.downloadsPerHourOfDayChart = getChart("discreteBarChart", $scope.downloadsPerHourOfDay, "hour", "count", "Hour of day", 'Downloads');
  1813. $scope.downloadsPerDayOfWeekChart = getChart("discreteBarChart", $scope.downloadsPerDayOfWeek, "day", "count", "Day of week", 'Downloads');
  1814. $scope.downloadsPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;
  1815.  
  1816. $scope.searchesPerHourOfDayChart = getChart("discreteBarChart", $scope.searchesPerHourOfDay, "hour", "count", "Hour of day", 'Searches');
  1817. $scope.searchesPerDayOfWeekChart = getChart("discreteBarChart", $scope.searchesPerDayOfWeek, "day", "count", "Day of week", 'Searches');
  1818. $scope.searchesPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;
  1819.  
  1820.  
  1821. //Was unable to use the function above for this and gave up
  1822. $scope.resultsSharesChart = {
  1823. options: {
  1824. chart: {
  1825. type: 'multiBarChart',
  1826. height: 350,
  1827. margin: {
  1828. top: 20,
  1829. right: 20,
  1830. bottom: 100,
  1831. left: 45
  1832. },
  1833.  
  1834. clipEdge: true,
  1835. duration: 500,
  1836. stacked: false,
  1837. reduceXTicks: false,
  1838. showValues: true,
  1839. tooltip: {
  1840. enabled: true,
  1841. valueFormatter: function (d) {
  1842. return d + "%";
  1843. }
  1844. },
  1845. showControls: false,
  1846. xAxis: {
  1847. axisLabel: '',
  1848. showMaxMin: false,
  1849. rotateLabels: 30,
  1850. axisLabelDistance: 30,
  1851. tickFormat: function (d) {
  1852. return d;
  1853. }
  1854. },
  1855. yAxis: {
  1856. axisLabel: 'Share (%)',
  1857. axisLabelDistance: -20,
  1858. tickFormat: function (d) {
  1859. return d;
  1860. }
  1861. }
  1862. }
  1863. },
  1864.  
  1865. data: [
  1866. {
  1867. key: "Results",
  1868. values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {
  1869. return {series: 0, y: stats.avgResultsShare, x: stats.name}
  1870. })
  1871. },
  1872. {
  1873. key: "Unique results",
  1874. values: _.map($scope.avgIndexerSearchResultsShares, function (stats) {
  1875. return {series: 1, y: stats.avgUniqueResults, x: stats.name}
  1876. })
  1877. }
  1878. ]
  1879. };
  1880.  
  1881. $scope.indexerDownloadSharesChart = {
  1882. options: {
  1883. chart: {
  1884. type: 'pieChart',
  1885. height: 500,
  1886. x: function (d) {
  1887. return d.name;
  1888. },
  1889. y: function (d) {
  1890. return d.share;
  1891. },
  1892. showLabels: true,
  1893. duration: 500,
  1894. labelThreshold: 0.01,
  1895. labelSunbeamLayout: true,
  1896. tooltip: {
  1897. valueFormatter: function (d, i) {
  1898. return $filter('number')(d, 2) + "%";
  1899. }
  1900. },
  1901. legend: {
  1902. margin: {
  1903. top: 5,
  1904. right: 35,
  1905. bottom: 5,
  1906. left: 0
  1907. }
  1908. }
  1909. }
  1910. },
  1911. data: $scope.indexerDownloadShares
  1912. };
  1913.  
  1914.  
  1915. }
  1916. StatsController.$inject = ["$scope", "$filter", "stats"];
  1917.  
  1918. //
  1919. angular
  1920. .module('nzbhydraApp')
  1921. .factory('SearchService', SearchService);
  1922.  
  1923. function SearchService($http) {
  1924.  
  1925.  
  1926. var lastExecutedQuery;
  1927. var lastResults;
  1928.  
  1929. return {
  1930. search: search,
  1931. getLastResults: getLastResults,
  1932. loadMore: loadMore
  1933. };
  1934.  
  1935.  
  1936. function search(category, query, tmdbid, title, tvdbid, rid, season, episode, minsize, maxsize, minage, maxage, indexers, mode) {
  1937. var uri;
  1938. if (category.indexOf("Movies") > -1 || (category.indexOf("20") == 0) || mode == "movie") {
  1939. console.log("Search for movies");
  1940. uri = new URI("internalapi/moviesearch");
  1941. if (angular.isDefined(tmdbid)) {
  1942. console.log("moviesearch per tmdbid");
  1943. uri.addQuery("tmdbid", tmdbid);
  1944. } else {
  1945. console.log("moviesearch per query");
  1946. uri.addQuery("query", query);
  1947. }
  1948.  
  1949. } else if (category.indexOf("TV") > -1 || (category.indexOf("50") == 0) || mode == "tvsearch") {
  1950. console.log("Search for shows");
  1951. uri = new URI("internalapi/tvsearch");
  1952. if (angular.isDefined(tvdbid)) {
  1953. uri.addQuery("tvdbid", tvdbid);
  1954. }
  1955. if (angular.isDefined(rid)) {
  1956. uri.addQuery("rid", rid);
  1957. } else {
  1958. console.log("tvsearch per query");
  1959. uri.addQuery("query", query);
  1960. }
  1961.  
  1962. if (angular.isDefined(season)) {
  1963. uri.addQuery("season", season);
  1964. }
  1965. if (angular.isDefined(episode)) {
  1966. uri.addQuery("episode", episode);
  1967. }
  1968. } else {
  1969. uri = new URI("internalapi/search");
  1970. uri.addQuery("query", query);
  1971. }
  1972. if (angular.isDefined(title)) {
  1973. uri.addQuery("title", title);
  1974. }
  1975. if (_.isNumber(minsize)) {
  1976. uri.addQuery("minsize", minsize);
  1977. }
  1978. if (_.isNumber(maxsize)) {
  1979. uri.addQuery("maxsize", maxsize);
  1980. }
  1981. if (_.isNumber(minage)) {
  1982. uri.addQuery("minage", minage);
  1983. }
  1984. if (_.isNumber(maxage)) {
  1985. uri.addQuery("maxage", maxage);
  1986. }
  1987. if (!angular.isUndefined(indexers)) {
  1988. uri.addQuery("indexers", decodeURIComponent(indexers));
  1989. }
  1990.  
  1991.  
  1992. uri.addQuery("category", category);
  1993. lastExecutedQuery = uri;
  1994. return $http.get(uri.toString()).then(processData);
  1995.  
  1996. }
  1997.  
  1998. function loadMore(offset, loadAll) {
  1999. lastExecutedQuery.removeQuery("offset");
  2000. lastExecutedQuery.addQuery("offset", offset);
  2001. lastExecutedQuery.addQuery("loadAll", loadAll ? true : false);
  2002.  
  2003. return $http.get(lastExecutedQuery.toString()).then(processData);
  2004. }
  2005.  
  2006. function processData(response) {
  2007. var results = response.data.results;
  2008. var indexersearches = response.data.indexersearches;
  2009. var total = response.data.total;
  2010. var rejected = response.data.rejected;
  2011. var resultsCount = results.length;
  2012.  
  2013.  
  2014. //Sum up response times of indexers from individual api accesses
  2015. //TODO: Move this to search result controller because we need to update it every time we loaded more results
  2016. _.each(indexersearches, function (ps) {
  2017. if (ps.did_search) {
  2018. ps.averageResponseTime = _.reduce(ps.apiAccesses, function (memo, rp) {
  2019. return memo + rp.response_time;
  2020. }, 0);
  2021. ps.averageResponseTime = ps.averageResponseTime / ps.apiAccesses.length;
  2022. }
  2023. });
  2024.  
  2025. lastResults = {"results": results, "indexersearches": indexersearches, "total": total, "resultsCount": resultsCount, "rejected": rejected};
  2026. return lastResults;
  2027. }
  2028.  
  2029. function getLastResults() {
  2030. return lastResults;
  2031. }
  2032. }
  2033. SearchService.$inject = ["$http"];
  2034. angular
  2035. .module('nzbhydraApp')
  2036. .controller('SearchResultsController', SearchResultsController);
  2037.  
  2038. function sumRejected(rejected) {
  2039. return _.reduce(rejected, function (memo, entry) {
  2040. return memo + entry[1];
  2041. }, 0);
  2042. }
  2043.  
  2044. //SearchResultsController.$inject = ['blockUi'];
  2045. function SearchResultsController($stateParams, $scope, $q, $timeout, blockUI, growl, localStorageService, SearchService, ConfigService) {
  2046.  
  2047. if (localStorageService.get("sorting") != null) {
  2048. var sorting = localStorageService.get("sorting");
  2049. $scope.sortPredicate = sorting.predicate;
  2050. $scope.sortReversed = sorting.reversed;
  2051. } else {
  2052. $scope.sortPredicate = "epoch";
  2053. $scope.sortReversed = true;
  2054. }
  2055. $scope.limitTo = 100;
  2056. $scope.offset = 0;
  2057. //Handle incoming data
  2058.  
  2059. $scope.indexersearches = _.sortBy(SearchService.getLastResults().indexersearches, function (i) {
  2060. return i.indexer.toLowerCase()
  2061. });
  2062. $scope.indexerDisplayState = []; //Stores if a indexer's results should be displayed or not
  2063. $scope.indexerResultsInfo = {}; //Stores information about the indexer's results like how many we already retrieved
  2064. $scope.groupExpanded = {};
  2065. $scope.selected = [];
  2066. $scope.lastClicked = null;
  2067. $scope.lastClickedValue = null;
  2068.  
  2069. $scope.foo = {
  2070. indexerStatusesExpanded: localStorageService.get("indexerStatusesExpanded") != null ? localStorageService.get("indexerStatusesExpanded") : false,
  2071. duplicatesDisplayed: localStorageService.get("duplicatesDisplayed") != null ? localStorageService.get("duplicatesDisplayed") : false
  2072. };
  2073.  
  2074. $scope.countFilteredOut = 0;
  2075.  
  2076. //Initially set visibility of all found indexers to true, they're needed for initial filtering / sorting
  2077. _.forEach($scope.indexersearches, function (ps) {
  2078. $scope.indexerDisplayState[ps.indexer.toLowerCase()] = true;
  2079. });
  2080.  
  2081. _.forEach($scope.indexersearches, function (ps) {
  2082. $scope.indexerResultsInfo[ps.indexer.toLowerCase()] = {loadedResults: ps.loaded_results};
  2083. });
  2084.  
  2085. //Process results
  2086. $scope.results = SearchService.getLastResults().results;
  2087. $scope.total = SearchService.getLastResults().total;
  2088. $scope.resultsCount = SearchService.getLastResults().resultsCount;
  2089. $scope.rejected = SearchService.getLastResults().rejected;
  2090. $scope.countRejected = sumRejected($scope.rejected);
  2091. $scope.filteredResults = sortAndFilter($scope.results);
  2092. stopBlocking();
  2093.  
  2094. //Returns the content of the property (defined by the current sortPredicate) of the first group element
  2095. $scope.firstResultPredicate = firstResultPredicate;
  2096. function firstResultPredicate(item) {
  2097. return item[0][$scope.sortPredicate];
  2098. }
  2099.  
  2100. //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...)
  2101. $scope.groupId = groupId;
  2102. function groupId(item) {
  2103. return item[0][0].searchResultId;
  2104. }
  2105.  
  2106. //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?
  2107. function startBlocking(message) {
  2108. var deferred = $q.defer();
  2109. blockUI.start(message);
  2110. $timeout(function () {
  2111. deferred.resolve();
  2112. }, 100);
  2113. return deferred.promise;
  2114. }
  2115.  
  2116. //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.)
  2117. //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
  2118. $scope.setSorting = setSorting;
  2119. function setSorting(predicate, reversedDefault) {
  2120. if (predicate == $scope.sortPredicate) {
  2121. $scope.sortReversed = !$scope.sortReversed;
  2122. } else {
  2123. $scope.sortReversed = reversedDefault;
  2124. }
  2125. $scope.sortPredicate = predicate;
  2126. startBlocking("Sorting / filtering...").then(function () {
  2127. $scope.filteredResults = sortAndFilter($scope.results);
  2128. blockUI.reset();
  2129. localStorageService.set("sorting", {predicate: predicate, reversed: $scope.sortReversed});
  2130. });
  2131. }
  2132.  
  2133. $scope.inlineFilter = inlineFilter;
  2134. function inlineFilter(result) {
  2135. var ok = true;
  2136. ok = ok && $scope.titleFilter && result.title.toLowerCase().indexOf($scope.titleFilter) > -1;
  2137. ok = ok && $scope.minSizeFilter && $scope.minSizeFilter * 1024 * 1024 < result.size;
  2138. ok = ok && $scope.maxSizeFilter && $scope.maxSizeFilter * 1024 * 1024 > result.size;
  2139. return ok;
  2140. }
  2141.  
  2142.  
  2143. $scope.$on("searchInputChanged", function (event, query, minage, maxage, minsize, maxsize) {
  2144. console.log("Got event searchInputChanged");
  2145. $scope.filteredResults = sortAndFilter($scope.results, query, minage, maxage, minsize, maxsize);
  2146. });
  2147.  
  2148. $scope.resort = function () {
  2149. };
  2150.  
  2151. function sortAndFilter(results, query, minage, maxage, minsize, maxsize) {
  2152. $scope.countFilteredOut = 0;
  2153.  
  2154. function filterByAgeAndSize(item) {
  2155. var ok = true;
  2156. ok = ok && (!_.isNumber(minsize) || item.size / 1024 / 1024 >= minsize)
  2157. && (!_.isNumber(maxsize) || item.size / 1024 / 1024 <= maxsize)
  2158. && (!_.isNumber(minage) || item.age_days >= Number(minage))
  2159. && (!_.isNumber(maxage) || item.age_days <= Number(maxage));
  2160.  
  2161. if (ok && query) {
  2162. var words = query.toLowerCase().split(" ");
  2163. ok = _.every(words, function (word) {
  2164. return item.title.toLowerCase().indexOf(word) > -1;
  2165. });
  2166. }
  2167. if (!ok) {
  2168. $scope.countFilteredOut++;
  2169. }
  2170. return ok;
  2171. }
  2172.  
  2173.  
  2174. function getItemIndexerDisplayState(item) {
  2175. return $scope.indexerDisplayState[item.indexer.toLowerCase()];
  2176. }
  2177.  
  2178. function getCleanedTitle(element) {
  2179. return element.title.toLowerCase().replace(/[\s\-\._]/ig, "");
  2180. }
  2181.  
  2182. function createSortedHashgroups(titleGroup) {
  2183.  
  2184. function createHashGroup(hashGroup) {
  2185. //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)
  2186. var sortedHashGroup = _.sortBy(hashGroup, function (item) {
  2187. var sortPredicateValue;
  2188. if ($scope.sortPredicate == "grabs") {
  2189. sortPredicateValue = angular.isDefined(item.grabs) ? item.grabs : 0;
  2190. } else {
  2191. sortPredicateValue = item[$scope.sortPredicate];
  2192. }
  2193. //var sortPredicateValue = item[$scope.sortPredicate];
  2194. return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;
  2195. });
  2196. //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)
  2197. sortedHashGroup = _.sortBy(sortedHashGroup, function (item) {
  2198. return item.indexerscore * -1;
  2199. });
  2200. return sortedHashGroup;
  2201. }
  2202.  
  2203. function getHashGroupFirstElementSortPredicate(hashGroup) {
  2204. if ($scope.sortPredicate == "grabs") {
  2205. sortPredicateValue = angular.isDefined(hashGroup[0].grabs) ? hashGroup[0].grabs : 0;
  2206. } else {
  2207. var sortPredicateValue = hashGroup[0][$scope.sortPredicate];
  2208. }
  2209. return $scope.sortReversed ? -sortPredicateValue : sortPredicateValue;
  2210. }
  2211.  
  2212. return _.chain(titleGroup).groupBy("hash").map(createHashGroup).sortBy(getHashGroupFirstElementSortPredicate).value();
  2213. }
  2214.  
  2215. function getTitleGroupFirstElementsSortPredicate(titleGroup) {
  2216. var sortPredicateValue;
  2217. if ($scope.sortPredicate == "title") {
  2218. sortPredicateValue = titleGroup[0][0].title.toLowerCase();
  2219. } else if ($scope.sortPredicate == "grabs") {
  2220. sortPredicateValue = angular.isDefined(titleGroup[0][0].grabs) ? titleGroup[0][0].grabs : 0;
  2221. } else {
  2222. sortPredicateValue = titleGroup[0][0][$scope.sortPredicate];
  2223. }
  2224.  
  2225. return sortPredicateValue;
  2226. }
  2227.  
  2228. var filtered = _.chain(results)
  2229. //Filter by age, size and title
  2230. .filter(filterByAgeAndSize)
  2231. //Remove elements of which the indexer is currently hidden
  2232. .filter(getItemIndexerDisplayState)
  2233. //Make groups of results with the same title
  2234. .groupBy(getCleanedTitle)
  2235. //For every title group make subgroups of duplicates and sort the group
  2236. .map(createSortedHashgroups)
  2237. //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)
  2238. .sortBy(getTitleGroupFirstElementsSortPredicate)
  2239. .value();
  2240. if ($scope.sortReversed) {
  2241. filtered = filtered.reverse();
  2242. }
  2243. if ($scope.countFilteredOut > 0) {
  2244. growl.info("Filtered " + $scope.countFilteredOut + " of the retrieved results");
  2245. }
  2246.  
  2247. $scope.lastClicked = null;
  2248. return filtered;
  2249. }
  2250.  
  2251. $scope.toggleTitlegroupExpand = function toggleTitlegroupExpand(titleGroup) {
  2252. $scope.groupExpanded[titleGroup[0][0].title] = !$scope.groupExpanded[titleGroup[0][0].title];
  2253. $scope.groupExpanded[titleGroup[0][0].hash] = !$scope.groupExpanded[titleGroup[0][0].hash];
  2254. };
  2255.  
  2256.  
  2257. $scope.stopBlocking = stopBlocking;
  2258. function stopBlocking() {
  2259. blockUI.reset();
  2260. }
  2261.  
  2262. $scope.loadMore = loadMore;
  2263. function loadMore(loadAll) {
  2264. startBlocking(loadAll ? "Loading all results..." : "Loading more results...").then(function () {
  2265. SearchService.loadMore($scope.resultsCount, loadAll).then(function (data) {
  2266. $scope.results = $scope.results.concat(data.results);
  2267. $scope.filteredResults = sortAndFilter($scope.results);
  2268. $scope.total = data.total;
  2269. $scope.rejected = data.rejected;
  2270. $scope.countRejected = sumRejected($scope.rejected);
  2271. $scope.resultsCount += data.resultsCount;
  2272. stopBlocking();
  2273. });
  2274. });
  2275. }
  2276.  
  2277.  
  2278. //Filters the results according to new visibility settings.
  2279. $scope.toggleIndexerDisplay = toggleIndexerDisplay;
  2280. function toggleIndexerDisplay(indexer) {
  2281. $scope.indexerDisplayState[indexer.toLowerCase()] = $scope.indexerDisplayState[indexer.toLowerCase()];
  2282. startBlocking("Filtering. Sorry...").then(function () {
  2283. $scope.filteredResults = sortAndFilter($scope.results);
  2284. }).then(function () {
  2285. stopBlocking();
  2286. });
  2287. }
  2288.  
  2289. $scope.countResults = countResults;
  2290. function countResults() {
  2291. return $scope.results.length;
  2292. }
  2293.  
  2294. $scope.invertSelection = function invertSelection() {
  2295. $scope.$broadcast("invertSelection");
  2296. };
  2297.  
  2298. $scope.toggleIndexerStatuses = function () {
  2299. $scope.foo.indexerStatusesExpanded = !$scope.foo.indexerStatusesExpanded;
  2300. localStorageService.set("indexerStatusesExpanded", $scope.foo.indexerStatusesExpanded);
  2301. };
  2302.  
  2303. $scope.toggleDuplicatesDisplayed = function () {
  2304. //$scope.foo.duplicatesDisplayed = !$scope.foo.duplicatesDisplayed;
  2305. localStorageService.set("duplicatesDisplayed", $scope.foo.duplicatesDisplayed);
  2306. $scope.$broadcast("duplicatesDisplayed", $scope.foo.duplicatesDisplayed);
  2307. };
  2308.  
  2309. $scope.$on("checkboxClicked", function (event, originalEvent, rowIndex, newCheckedValue) {
  2310. if (originalEvent.shiftKey && $scope.lastClicked != null) {
  2311. console.log("Shift clicked from " + $scope.lastClicked + " to " + rowIndex);
  2312. $scope.$broadcast("shiftClick", Number($scope.lastClicked), Number(rowIndex), Number($scope.lastClickedValue));
  2313. }
  2314. $scope.lastClicked = rowIndex;
  2315. $scope.lastClickedValue = newCheckedValue;
  2316. })
  2317.  
  2318. $scope.filterRejectedZero = function() {
  2319. return function (entry) {
  2320. return entry[1] > 0;
  2321. }
  2322. }
  2323. }
  2324. SearchResultsController.$inject = ["$stateParams", "$scope", "$q", "$timeout", "blockUI", "growl", "localStorageService", "SearchService", "ConfigService"];
  2325.  
  2326.  
  2327. angular
  2328. .module('nzbhydraApp')
  2329. .controller('SearchHistoryController', SearchHistoryController);
  2330.  
  2331.  
  2332. function SearchHistoryController($scope, $state, StatsService, history, $sce, $filter) {
  2333. $scope.type = "All";
  2334. $scope.limit = 100;
  2335. $scope.pagination = {
  2336. current: 1
  2337. };
  2338. $scope.isLoaded = true;
  2339. $scope.searchRequests = history.data.searchRequests;
  2340. $scope.totalRequests = history.data.totalRequests;
  2341.  
  2342. $scope.pageChanged = function (newPage) {
  2343. getSearchRequestsPage(newPage);
  2344. };
  2345.  
  2346. $scope.changeType = function (type) {
  2347. $scope.type = type;
  2348. getSearchRequestsPage($scope.pagination.current);
  2349. };
  2350.  
  2351. function getSearchRequestsPage(pageNumber) {
  2352. StatsService.getSearchHistory(pageNumber, $scope.limit, $scope.type).then(function (history) {
  2353. $scope.searchRequests = history.data.searchRequests;
  2354. $scope.totalRequests = history.data.totalRequests;
  2355. $scope.isLoaded = true;
  2356. });
  2357. }
  2358.  
  2359. $scope.openSearch = function (request) {
  2360. var stateParams = {};
  2361. if (request.identifier_key == "imdbid") {
  2362. stateParams.imdbid = request.identifier_value;
  2363. } else if (request.identifier_key == "tvdbid" || request.identifier_key == "rid") {
  2364. if (request.identifier_key == "rid") {
  2365. stateParams.rid = request.identifier_value;
  2366. } else {
  2367. stateParams.tvdbid = request.identifier_value;
  2368. }
  2369.  
  2370. if (request.season != "") {
  2371. stateParams.season = request.season;
  2372. }
  2373. if (request.episode != "") {
  2374. stateParams.episode = request.episode;
  2375. }
  2376. }
  2377. if (request.query != "") {
  2378. stateParams.query = request.query;
  2379. }
  2380. if (request.type == "tv") {
  2381. stateParams.mode = "tvsearch"
  2382. } else if (request.type == "tv") {
  2383. stateParams.mode = "movie"
  2384. } else {
  2385. stateParams.mode = "search"
  2386. }
  2387.  
  2388. if (request.movietitle != null) {
  2389. stateParams.title = request.movietitle;
  2390. }
  2391. if (request.tvtitle != null) {
  2392. stateParams.title = request.tvtitle;
  2393. }
  2394.  
  2395. if (request.category) {
  2396. stateParams.category = request.category;
  2397. }
  2398.  
  2399. stateParams.category = request.category;
  2400.  
  2401. $state.go("root.search", stateParams, {inherit: false});
  2402. };
  2403.  
  2404. $scope.formatQuery = function (request) {
  2405. if (request.movietitle != null) {
  2406. return request.movietitle;
  2407. }
  2408. if (request.tvtitle != null) {
  2409. return request.tvtitle;
  2410. }
  2411.  
  2412. if (!request.query && !request.identifier_key && !request.season && !request.episode) {
  2413. return "Update query";
  2414. }
  2415. return request.query;
  2416. };
  2417.  
  2418. $scope.formatAdditional = function(request) {
  2419. var result = [];
  2420. //ID key: ID value
  2421. //season
  2422. //episode
  2423. //author
  2424. //title
  2425. if (request.identifier_key) {
  2426. var href;
  2427. var key;
  2428. if (request.identifier_key == "imdbid") {
  2429. key = "IMDB ID";
  2430. href = "https://www.imdb.com/title/tt"
  2431. } else if (request.identifier_key == "tvdbid") {
  2432. key = "TVDB ID";
  2433. href = "https://thetvdb.com/?tab=series&id="
  2434. } else if (request.identifier_key == "rid") {
  2435. key = "TVRage ID";
  2436. href = "internalapi/redirect_rid?rid="
  2437. } else if (request.identifier_key == "tmdb") {
  2438. key = "TMDV ID";
  2439. href = "https://www.themoviedb.org/movie/"
  2440. }
  2441. href = href + request.identifier_value;
  2442. href = $filter("dereferer")(href);
  2443. result.push(key + ": " + '<a target="_blank" href="' + href + '">' + request.identifier_value + "</a>");
  2444. }
  2445. if (request.season) {
  2446. result.push("Season: " + request.season);
  2447. }
  2448. if (request.episode) {
  2449. result.push("Episode: " + request.episode);
  2450. }
  2451. if (request.author) {
  2452. result.push("Author: " + request.author);
  2453. }
  2454. if (request.title) {
  2455. result.push("Title: " + request.title);
  2456. }
  2457. return $sce.trustAsHtml(result.join(", "));
  2458. };
  2459.  
  2460.  
  2461. }
  2462. SearchHistoryController.$inject = ["$scope", "$state", "StatsService", "history", "$sce", "$filter"];
  2463.  
  2464. angular
  2465. .module('nzbhydraApp')
  2466. .controller('SearchController', SearchController);
  2467.  
  2468. function SearchController($scope, $http, $stateParams, $state, SearchService, focus, ConfigService, CategoriesService, blockUI, $element) {
  2469.  
  2470. function getNumberOrUndefined(number) {
  2471. if (_.isUndefined(number) || _.isNaN(number) || number == "") {
  2472. return undefined;
  2473. }
  2474. number = parseInt(number);
  2475. if (_.isNumber(number)) {
  2476. return number;
  2477. } else {
  2478. return undefined;
  2479. }
  2480. }
  2481.  
  2482. //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)
  2483. $scope.mode = $stateParams.mode;
  2484. $scope.categories = _.filter(CategoriesService.getAll(), function(c) {
  2485. return c.mayBeSelected && c.ignoreResults != "internal" && c.ignoreResults != "always";
  2486. });
  2487. if (angular.isDefined($stateParams.category) && $stateParams.category) {
  2488. $scope.category = CategoriesService.getByName($stateParams.category);
  2489. } else {
  2490. $scope.category = CategoriesService.getDefault();
  2491. }
  2492. $scope.category = (_.isUndefined($stateParams.category) || $stateParams.category == "") ? CategoriesService.getDefault() : CategoriesService.getByName($stateParams.category);
  2493. $scope.tmdbid = $stateParams.tmdbid;
  2494. $scope.tvdbid = $stateParams.tvdbid;
  2495. $scope.rid = $stateParams.rid;
  2496. $scope.title = $stateParams.title;
  2497. $scope.season = $stateParams.season;
  2498. $scope.episode = $stateParams.episode;
  2499. $scope.query = $stateParams.query;
  2500. $scope.minsize = getNumberOrUndefined($stateParams.minsize);
  2501. $scope.maxsize = getNumberOrUndefined($stateParams.maxsize);
  2502. $scope.minage = getNumberOrUndefined($stateParams.minage);
  2503. $scope.maxage = getNumberOrUndefined($stateParams.maxage);
  2504. if (!_.isUndefined($scope.title) && _.isUndefined($scope.query)) {
  2505. //$scope.query = $scope.title;
  2506. }
  2507. if (!angular.isUndefined($stateParams.indexers)) {
  2508. $scope.indexers = decodeURIComponent($stateParams.indexers).split("|");
  2509. }
  2510.  
  2511. $scope.showIndexers = {};
  2512.  
  2513. var safeConfig = ConfigService.getSafe();
  2514.  
  2515.  
  2516. $scope.typeAheadWait = 300;
  2517. $scope.selectedItem = "";
  2518. $scope.autocompleteLoading = false;
  2519. $scope.isAskById = $scope.category.supportsById;
  2520. $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
  2521. $scope.availableIndexers = [];
  2522. $scope.autocompleteClass = "autocompletePosterMovies";
  2523.  
  2524. $scope.toggle = function (searchCategory) {
  2525. $scope.category = searchCategory;
  2526.  
  2527. //Show checkbox to ask if the user wants to search by ID (using autocomplete)
  2528. $scope.isAskById = $scope.category.supportsById;
  2529.  
  2530. focus('focus-query-box');
  2531.  
  2532. //Hacky way of triggering the autocomplete loading
  2533. var searchModel = $element.find("#searchfield").controller("ngModel");
  2534. if (angular.isDefined(searchModel.$viewValue)) {
  2535. searchModel.$setViewValue(searchModel.$viewValue + " ");
  2536. }
  2537.  
  2538. if (safeConfig.searching.enableCategorySizes) {
  2539. var min = searchCategory.min;
  2540. var max = searchCategory.max;
  2541. if (_.isNumber(min)) {
  2542. $scope.minsize = min;
  2543. } else {
  2544. $scope.minsize = "";
  2545. }
  2546. if (_.isNumber(max)) {
  2547. $scope.maxsize = max;
  2548. } else {
  2549. $scope.maxsize = "";
  2550. }
  2551. }
  2552.  
  2553. $scope.availableIndexers = getAvailableIndexers();
  2554.  
  2555.  
  2556. };
  2557.  
  2558.  
  2559. // Any function returning a promise object can be used to load values asynchronously
  2560. $scope.getAutocomplete = function (val) {
  2561. $scope.autocompleteLoading = true;
  2562. //Expected model returned from API:
  2563. //label: What to show in the results
  2564. //title: Will be used for file search
  2565. //value: Will be used as extraInfo (ttid oder tvdb id)
  2566. //poster: url of poster to show
  2567.  
  2568. //Don't use autocomplete if checkbox is disabled
  2569. if (!$scope.isById.value) {
  2570. return {};
  2571. }
  2572.  
  2573. if ($scope.category.name.indexOf("movies") > -1) {
  2574. return $http.get('internalapi/autocomplete?type=movie', {
  2575. params: {
  2576. input: val
  2577. }
  2578. }).then(function (response) {
  2579. $scope.autocompleteLoading = false;
  2580. return response.data.results;
  2581. });
  2582. } else if ($scope.category.name.indexOf("tv") > -1) {
  2583.  
  2584. return $http.get('internalapi/autocomplete?type=tv', {
  2585. params: {
  2586. input: val
  2587. }
  2588. }).then(function (response) {
  2589. $scope.autocompleteLoading = false;
  2590. return response.data.results;
  2591. });
  2592. } else {
  2593. return {};
  2594. }
  2595. };
  2596.  
  2597.  
  2598. $scope.startSearch = function () {
  2599. blockUI.start("Searching...");
  2600. var indexers = angular.isUndefined($scope.indexers) ? undefined : $scope.indexers.join("|");
  2601. 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 () {
  2602. $state.go("root.search.results", {
  2603. minsize: $scope.minsize,
  2604. maxsize: $scope.maxsize,
  2605. minage: $scope.minage,
  2606. maxage: $scope.maxage
  2607. }, {
  2608. inherit: true
  2609. });
  2610. $scope.tmdbid = undefined;
  2611. $scope.tvdbid = undefined;
  2612. });
  2613. };
  2614.  
  2615. function getSelectedIndexers() {
  2616. var activatedIndexers = _.filter($scope.availableIndexers).filter(function (indexer) {
  2617. return indexer.activated ;
  2618. });
  2619. return _.pluck(activatedIndexers, "name").join("|");
  2620. }
  2621.  
  2622.  
  2623. $scope.goToSearchUrl = function () {
  2624. var stateParams = {};
  2625. if ($scope.category.name.indexOf("movies") > -1) {
  2626. stateParams.title = $scope.title;
  2627. stateParams.mode = "movie";
  2628. } else if ($scope.category.name.indexOf("tv") > -1) {
  2629. stateParams.mode = "tvsearch";
  2630. stateParams.title = $scope.title;
  2631. } else if ($scope.category.name == "ebook") {
  2632. stateParams.mode = "ebook";
  2633. } else {
  2634. stateParams.mode = "search";
  2635. }
  2636.  
  2637. stateParams.tmdbid = $scope.tmdbid;
  2638. stateParams.tvdbid = $scope.tvdbid;
  2639. stateParams.title = $scope.title;
  2640. stateParams.season = $scope.season;
  2641. stateParams.episode = $scope.episode;
  2642. stateParams.query = $scope.query;
  2643. stateParams.minsize = $scope.minsize;
  2644. stateParams.maxsize = $scope.maxsize;
  2645. stateParams.minage = $scope.minage;
  2646. stateParams.maxage = $scope.maxage;
  2647. stateParams.category = $scope.category.name;
  2648. stateParams.indexers = encodeURIComponent(getSelectedIndexers());
  2649.  
  2650. $state.go("root.search", stateParams, {inherit: false, notify: true, reload: true});
  2651. };
  2652.  
  2653.  
  2654. $scope.selectAutocompleteItem = function ($item) {
  2655. $scope.selectedItem = $item;
  2656. $scope.title = $item.title;
  2657. if ($scope.category.name.indexOf("movies") > -1) {
  2658. $scope.tmdbid = $item.value;
  2659. } else if ($scope.category.name.indexOf("tv") > -1) {
  2660. $scope.tvdbid = $item.value;
  2661. }
  2662. $scope.query = "";
  2663. $scope.goToSearchUrl();
  2664. };
  2665.  
  2666. $scope.startQuerySearch = function() {
  2667. //Reset values because they might've been set from the last search
  2668. $scope.title = undefined;
  2669. $scope.tmdbid = undefined;
  2670. $scope.tvdbid = undefined;
  2671. $scope.goToSearchUrl();
  2672. };
  2673.  
  2674.  
  2675. $scope.autocompleteActive = function () {
  2676. return $scope.category.supportsById;
  2677. };
  2678.  
  2679. $scope.seriesSelected = function () {
  2680. return $scope.category.name.indexOf("tv") > -1;
  2681. };
  2682.  
  2683. $scope.toggleIndexer = function(indexer) {
  2684. $scope.indexers[indexer] = !$scope.indexers[indexer]
  2685. };
  2686.  
  2687.  
  2688. function isIndexerPreselected(indexer) {
  2689. if (angular.isUndefined($scope.indexers)) {
  2690. return indexer.preselect;
  2691. } else {
  2692. return _.contains($scope.indexers, indexer.name);
  2693. }
  2694.  
  2695. }
  2696.  
  2697.  
  2698. function getAvailableIndexers() {
  2699. return _.chain(safeConfig.indexers).filter(function (indexer) {
  2700. return indexer.enabled && indexer.showOnSearch && (angular.isUndefined(indexer.categories) || indexer.categories.length == 0 || $scope.category.name == "all" || indexer.categories.indexOf($scope.category.name) > -1);
  2701. }).sortBy(function(indexer) {
  2702. return indexer.name.toLowerCase();
  2703. })
  2704. .map(function (indexer) {
  2705. return {name: indexer.name, activated: isIndexerPreselected(indexer), categories: indexer.categories};
  2706. }).value();
  2707. }
  2708.  
  2709. $scope.toggleAllIndexers = function() {
  2710. angular.forEach($scope.availableIndexers, function(indexer) {
  2711. indexer.activated = !indexer.activated;
  2712. })
  2713. };
  2714.  
  2715. $scope.searchInputChanged = function() {
  2716. $scope.$broadcast("searchInputChanged", $scope.query != $stateParams.query ? $scope.query : null, $scope.minage, $scope.maxage, $scope.minsize, $scope.maxsize);
  2717. };
  2718.  
  2719. $scope.availableIndexers = getAvailableIndexers();
  2720.  
  2721.  
  2722. if ($scope.mode) {
  2723. $scope.startSearch();
  2724. }
  2725.  
  2726. }
  2727. SearchController.$inject = ["$scope", "$http", "$stateParams", "$state", "SearchService", "focus", "ConfigService", "CategoriesService", "blockUI", "$element"];
  2728.  
  2729. angular
  2730. .module('nzbhydraApp')
  2731. .factory('RestartService', RestartService);
  2732.  
  2733. function RestartService(blockUI, $timeout, $window, NzbHydraControlService) {
  2734.  
  2735. return {
  2736. restart: restart
  2737. };
  2738.  
  2739.  
  2740. function internalCaR(message, timer) {
  2741.  
  2742. if (timer >= 1) {
  2743. blockUI.start(message + "Restarting. Will reload page in " + timer + " seconds...");
  2744. $timeout(function () {
  2745. internalCaR(message, timer - 1)
  2746. }, 1000);
  2747. } else {
  2748. $timeout(function () {
  2749. blockUI.start("Reloading page...");
  2750. $window.location.reload();
  2751. }, 1000);
  2752. }
  2753. }
  2754.  
  2755.  
  2756.  
  2757. function restart(message) {
  2758. message = angular.isDefined(message) ? message + " " : "";
  2759. NzbHydraControlService.restart().then(internalCaR(message, 15),
  2760. function () {
  2761. growl.info("Unable to send restart command.");
  2762. }
  2763. )
  2764. }
  2765. }
  2766. RestartService.$inject = ["blockUI", "$timeout", "$window", "NzbHydraControlService"];
  2767.  
  2768. angular
  2769. .module('nzbhydraApp')
  2770. .factory('NzbHydraControlService', NzbHydraControlService);
  2771.  
  2772. function NzbHydraControlService($http) {
  2773.  
  2774. return {
  2775. restart: restart,
  2776. shutdown: shutdown
  2777. };
  2778.  
  2779. function restart() {
  2780. return $http.get("internalapi/restart");
  2781. }
  2782.  
  2783. function shutdown() {
  2784. return $http.get("internalapi/shutdown");
  2785. }
  2786. }
  2787. NzbHydraControlService.$inject = ["$http"];
  2788.  
  2789. angular
  2790. .module('nzbhydraApp')
  2791. .factory('NzbDownloadService', NzbDownloadService);
  2792.  
  2793. function NzbDownloadService($http, ConfigService, DownloaderCategoriesService) {
  2794.  
  2795. var service = {
  2796. download: download,
  2797. getEnabledDownloaders: getEnabledDownloaders
  2798. };
  2799.  
  2800. return service;
  2801.  
  2802. function sendNzbAddCommand(downloader, searchresultids, category) {
  2803. return $http.put("internalapi/addnzbs", {downloader: downloader.name, searchresultids: angular.toJson(searchresultids), category: category});
  2804. }
  2805.  
  2806. function download(downloader, searchresultids) {
  2807.  
  2808. var category = downloader.defaultCategory;
  2809.  
  2810. if (_.isUndefined(category) || category == "" || category == null) {
  2811. return DownloaderCategoriesService.openCategorySelection(downloader).then(function (category) {
  2812. return sendNzbAddCommand(downloader, searchresultids, category)
  2813. }, function (error) {
  2814. throw error;
  2815. });
  2816. } else {
  2817. return sendNzbAddCommand(downloader, searchresultids, category)
  2818. }
  2819. }
  2820.  
  2821. function getEnabledDownloaders() {
  2822. return _.filter(ConfigService.getSafe().downloaders, "enabled");
  2823. }
  2824. }
  2825. NzbDownloadService.$inject = ["$http", "ConfigService", "DownloaderCategoriesService"];
  2826.  
  2827.  
  2828. angular
  2829. .module('nzbhydraApp')
  2830. .factory('ModalService', ModalService);
  2831.  
  2832. function ModalService($uibModal, $q) {
  2833.  
  2834. return {
  2835. open: open
  2836. };
  2837.  
  2838. function open(headline, message, params, size) {
  2839. //params example:
  2840. /*
  2841. var p =
  2842. {
  2843. yes: {
  2844. text: "Yes", //default: Ok
  2845. onYes: function() {}
  2846. },
  2847. no: { //default: Empty
  2848. text: "No",
  2849. onNo: function () {
  2850. }
  2851. },
  2852. cancel: {
  2853. text: "Cancel", //default: Cancel
  2854. onCancel: function () {
  2855. }
  2856. }
  2857. };
  2858. */
  2859. var modalInstance = $uibModal.open({
  2860. templateUrl: 'static/html/modal.html',
  2861. controller: 'ModalInstanceCtrl',
  2862. size: angular.isDefined(size) ? size : "md",
  2863. resolve: {
  2864. headline: function () {
  2865. return headline;
  2866. },
  2867. message: function(){
  2868. return message;
  2869. },
  2870. params: function() {
  2871. return params;
  2872. }
  2873. }
  2874. });
  2875.  
  2876. modalInstance.result.then(function() {
  2877.  
  2878. }, function() {
  2879.  
  2880. });
  2881. }
  2882.  
  2883. }
  2884. ModalService.$inject = ["$uibModal", "$q"];
  2885.  
  2886. angular
  2887. .module('nzbhydraApp')
  2888. .controller('ModalInstanceCtrl', ModalInstanceCtrl);
  2889.  
  2890. function ModalInstanceCtrl($scope, $uibModalInstance, headline, message, params) {
  2891.  
  2892. $scope.message = message;
  2893. $scope.headline = headline;
  2894. $scope.params = params;
  2895. $scope.showCancel = angular.isDefined(params) && angular.isDefined(params.cancel);
  2896. $scope.showNo = angular.isDefined(params) && angular.isDefined(params.no);
  2897.  
  2898. if (angular.isUndefined(params) || angular.isUndefined(params.yes)) {
  2899. $scope.params = {
  2900. yes: {
  2901. text: "Ok"
  2902. }
  2903. }
  2904. } else if (angular.isUndefined(params.yes.text)) {
  2905. params.yes.text = "Yes";
  2906. }
  2907.  
  2908. if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isUndefined($scope.params.no.text)) {
  2909. $scope.params.no.text = "No";
  2910. }
  2911.  
  2912. if (angular.isDefined(params) && angular.isDefined(params.cancel) && angular.isUndefined($scope.params.cancel.text)) {
  2913. $scope.params.cancel.text = "Cancel";
  2914. }
  2915.  
  2916. $scope.yes = function () {
  2917. $uibModalInstance.close();
  2918. if(angular.isDefined(params) && angular.isDefined(params.yes) && angular.isDefined($scope.params.yes.onYes)) {
  2919. $scope.params.yes.onYes();
  2920. }
  2921. };
  2922.  
  2923. $scope.no = function () {
  2924. $uibModalInstance.close();
  2925. if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isDefined($scope.params.no.onNo)) {
  2926. $scope.params.no.onNo();
  2927. }
  2928. };
  2929.  
  2930. $scope.cancel = function () {
  2931. $uibModalInstance.dismiss();
  2932. if (angular.isDefined(params.cancel) && angular.isDefined($scope.params.cancel.onCancel)) {
  2933. $scope.params.cancel.onCancel();
  2934. }
  2935. };
  2936.  
  2937. $scope.$on("modal.closing", function (targetScope, reason, c) {
  2938. if (reason == "backdrop click") {
  2939. $scope.cancel();
  2940. }
  2941. });
  2942. }
  2943. ModalInstanceCtrl.$inject = ["$scope", "$uibModalInstance", "headline", "message", "params"];
  2944.  
  2945. angular
  2946. .module('nzbhydraApp')
  2947. .service('GeneralModalService', GeneralModalService);
  2948.  
  2949. function GeneralModalService() {
  2950.  
  2951.  
  2952. this.open = function (msg, template, templateUrl, size, data) {
  2953.  
  2954. //Prevent circular dependency
  2955. var myInjector = angular.injector(["ng", "ui.bootstrap"]);
  2956. var $uibModal = myInjector.get("$uibModal");
  2957. var params = {};
  2958.  
  2959. if(angular.isUndefined(size)) {
  2960. params["size"] = size;
  2961. }
  2962. if (angular.isUndefined(template)) {
  2963. if (angular.isUndefined(templateUrl)) {
  2964. params["template"] = '<pre>' + msg + '</pre>';
  2965. } else {
  2966. params["templateUrl"] = templateUrl;
  2967. }
  2968. } else {
  2969. params["template"] = template;
  2970. }
  2971. params["resolve"] =
  2972. {
  2973. data: function () {
  2974. return data;
  2975. }
  2976. };
  2977.  
  2978. var modalInstance = $uibModal.open(params);
  2979.  
  2980. modalInstance.result.then();
  2981.  
  2982. };
  2983.  
  2984.  
  2985. }
  2986. angular
  2987. .module('nzbhydraApp')
  2988. .controller('LoginController', LoginController);
  2989.  
  2990. function LoginController($scope, RequestsErrorHandler, $state, HydraAuthService, $auth, growl) {
  2991. $scope.user = {};
  2992. $scope.login = function () {
  2993. RequestsErrorHandler.specificallyHandled(function () {
  2994. $auth.login($scope.user).then(function (data) {
  2995. HydraAuthService.setLoggedInByForm();
  2996. growl.info("Login successful!");
  2997. $state.go("root.search");
  2998. }, function () {
  2999. growl.error("Login failed!")
  3000. });
  3001. });
  3002. }
  3003. }
  3004. LoginController.$inject = ["$scope", "RequestsErrorHandler", "$state", "HydraAuthService", "$auth", "growl"];
  3005.  
  3006. angular
  3007. .module('nzbhydraApp')
  3008. .controller('IndexerStatusesController', IndexerStatusesController);
  3009.  
  3010. function IndexerStatusesController($scope, $http, statuses) {
  3011. $scope.statuses = statuses.data.indexerStatuses;
  3012.  
  3013. $scope.isInPast = function (timestamp) {
  3014. return timestamp * 1000 < (new Date).getTime();
  3015. };
  3016.  
  3017. $scope.enable = function(indexerName) {
  3018. $http.get("internalapi/enableindexer", {params: {name: indexerName}}).then(function(response){
  3019. $scope.statuses = response.data.indexerStatuses;
  3020. });
  3021. }
  3022.  
  3023. }
  3024. IndexerStatusesController.$inject = ["$scope", "$http", "statuses"];
  3025.  
  3026.  
  3027. angular
  3028. .module('nzbhydraApp')
  3029. .filter('formatDate', formatDate);
  3030.  
  3031. function formatDate(dateFilter) {
  3032. return function(timestamp, hidePast) {
  3033. if (timestamp) {
  3034. if (timestamp * 1000 < (new Date).getTime() && hidePast) {
  3035. return ""; //
  3036. }
  3037.  
  3038. var t = timestamp * 1000;
  3039. t = dateFilter(t, 'yyyy-MM-dd HH:mm');
  3040. return t;
  3041. } else {
  3042. return "";
  3043. }
  3044. }
  3045. }
  3046. formatDate.$inject = ["dateFilter"];
  3047.  
  3048. angular
  3049. .module('nzbhydraApp')
  3050. .filter('reformatDate', reformatDate);
  3051.  
  3052. function reformatDate() {
  3053. return function (date) {
  3054. //Date in database is saved as UTC without timezone information
  3055. return moment.utc(date, "ddd, D MMM YYYY HH:mm:ss z").local().format("YYYY-MM-DD HH:mm");
  3056.  
  3057. }
  3058. }
  3059. angular
  3060. .module('nzbhydraApp')
  3061. .controller('IndexController', IndexController);
  3062.  
  3063. function IndexController($scope, $http, $stateParams, $state) {
  3064. console.log("Index");
  3065. $state.go("root.search");
  3066. }
  3067. IndexController.$inject = ["$scope", "$http", "$stateParams", "$state"];
  3068.  
  3069. angular
  3070. .module('nzbhydraApp')
  3071. .factory('HydraAuthService', HydraAuthService);
  3072.  
  3073. function HydraAuthService($auth, $q, $rootScope, ConfigService, bootstrapped) {
  3074.  
  3075. var loggedIn = false;
  3076. var username;
  3077. var maySeeAdmin = bootstrapped.maySeeAdmin;
  3078. var maySeeStats = bootstrapped.maySeeStats;
  3079.  
  3080. return {
  3081. isLoggedIn: isLoggedIn,
  3082. login: login,
  3083. logout: logout,
  3084. setLoggedInByForm: setLoggedInByForm,
  3085. getUserRights: getUserRights,
  3086. setLoggedInByBasic: setLoggedInByBasic,
  3087. getUserName: getUserName
  3088. };
  3089.  
  3090. function isLoggedIn() {
  3091. return loggedIn || (ConfigService.getSafe().authType == "form" && $auth.isAuthenticated()) || ConfigService.getSafe().authType == "none";
  3092. }
  3093.  
  3094. function setLoggedInByForm() {
  3095. maySeeStats = $auth.getPayload().maySeeStats;
  3096. maySeeAdmin = $auth.getPayload().maySeeAdmin;
  3097. username = $auth.getPayload().username;
  3098. loggedIn = true;
  3099. $rootScope.$broadcast("user:loggedIn", {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin});
  3100. }
  3101.  
  3102. function setLoggedInByBasic(_maySeeStats, _maySeeAdmin, _username) {
  3103. maySeeAdmin = _maySeeAdmin;
  3104. maySeeStats = _maySeeStats;
  3105. username = _username;
  3106. loggedIn = true;
  3107. $rootScope.$broadcast("user:loggedIn", {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin});
  3108. }
  3109.  
  3110. function login(user) {
  3111. var deferred = $q.defer();
  3112. $auth.login(user).then(function (data) {
  3113.  
  3114. $rootScope.$broadcast("user:loggedIn", data);
  3115. deferred.resolve();
  3116. });
  3117. return deferred;
  3118. }
  3119.  
  3120. function logout() {
  3121. $auth.logout();
  3122. loggedIn = false;
  3123. $rootScope.$broadcast("user:loggedOut");
  3124. }
  3125.  
  3126. function getUserRights() {
  3127. return {maySeeStats: maySeeStats, maySeeAdmin: maySeeAdmin};
  3128. }
  3129.  
  3130. function getUserName() {
  3131. return username;
  3132. }
  3133.  
  3134.  
  3135.  
  3136.  
  3137. }
  3138. HydraAuthService.$inject = ["$auth", "$q", "$rootScope", "ConfigService", "bootstrapped"];
  3139. angular
  3140. .module('nzbhydraApp')
  3141. .controller('HeaderController', HeaderController);
  3142.  
  3143. function HeaderController($scope, $state, $http, growl, HydraAuthService, ConfigService, bootstrapped) {
  3144.  
  3145. $scope.showLoginout = false;
  3146.  
  3147. if (ConfigService.getSafe().authType == "none") {
  3148. $scope.showAdmin = true;
  3149. $scope.showStats = true;
  3150. $scope.showLoginout = false;
  3151. } else {
  3152. if (HydraAuthService.isLoggedIn()) {
  3153. var rights = HydraAuthService.getUserRights();
  3154. $scope.showAdmin = rights.maySeeAdmin;
  3155. $scope.showStats = rights.maySeeStats;
  3156. $scope.loginlogoutText = "Logout";
  3157. $scope.showLoginout = true;
  3158. } else {
  3159. $scope.showAdmin = !bootstrapped.adminRestricted;
  3160. $scope.showStats = !bootstrapped.statsRestricted;
  3161. $scope.loginlogoutText = "Login";
  3162. $scope.showLoginout = bootstrapped.adminRestricted || bootstrapped.statsRestricted || bootstrapped.searchRestricted;
  3163. }
  3164. }
  3165.  
  3166. function onLogin(data) {
  3167. $scope.showAdmin = data.maySeeAdmin;
  3168. $scope.showStats = data.maySeeStats;
  3169. $scope.showLoginout = true;
  3170. $scope.loginlogoutText = "Logout";
  3171. }
  3172.  
  3173. $scope.$on("user:loggedIn", function (event, data) {
  3174. onLogin(data);
  3175. });
  3176.  
  3177. function onLogout() {
  3178. $scope.showAdmin = !bootstrapped.adminRestricted;
  3179. $scope.showStats = !bootstrapped.statsRestricted;
  3180. $scope.loginlogoutText = "Login";
  3181. $scope.showLoginout = bootstrapped.adminRestricted || bootstrapped.statsRestricted || bootstrapped.searchRestricted;
  3182. }
  3183.  
  3184. $scope.$on("user:loggedOut", function (event, data) {
  3185. onLogout();
  3186. });
  3187.  
  3188. $scope.loginout = function () {
  3189. if (HydraAuthService.isLoggedIn()) {
  3190. HydraAuthService.logout();
  3191.  
  3192. if (ConfigService.getSafe().authType == "basic") {
  3193. growl.info("Logged out. Close your browser to make sure session is closed.");
  3194. }
  3195. else if (ConfigService.getSafe().authType == "form") {
  3196. growl.info("Logged out");
  3197. }
  3198. onLogout();
  3199. $state.go("root.search");
  3200. } else {
  3201. if (ConfigService.getSafe().authType == "basic") {
  3202. var params = {};
  3203. if (HydraAuthService.getUserName()) {
  3204. params = {
  3205. old_username: HydraAuthService.getUserName()
  3206. }
  3207. }
  3208. $http.get("internalapi/askforpassword", {params: params}).then(function () {
  3209. growl.info("Login successful!");
  3210. //onLogin();
  3211. $state.go("root.search");
  3212. })
  3213. } else if (ConfigService.getSafe().authType == "form") {
  3214. $state.go("root.login");
  3215. } else {
  3216. growl.info("You shouldn't need to login but here you go!");
  3217. }
  3218. }
  3219. }
  3220. }
  3221. HeaderController.$inject = ["$scope", "$state", "$http", "growl", "HydraAuthService", "ConfigService", "bootstrapped"];
  3222.  
  3223. var HEADER_NAME = 'MyApp-Handle-Errors-Generically';
  3224. var specificallyHandleInProgress = false;
  3225.  
  3226. nzbhydraapp.factory('RequestsErrorHandler', ["$q", "growl", "blockUI", "GeneralModalService", function ($q, growl, blockUI, GeneralModalService) {
  3227. return {
  3228. // --- The user's API for claiming responsiblity for requests ---
  3229. specificallyHandled: function (specificallyHandledBlock) {
  3230. specificallyHandleInProgress = true;
  3231. try {
  3232. return specificallyHandledBlock();
  3233. } finally {
  3234. specificallyHandleInProgress = false;
  3235. }
  3236. },
  3237.  
  3238. // --- Response interceptor for handling errors generically ---
  3239. responseError: function (rejection) {
  3240. blockUI.reset();
  3241. var shouldHandle = (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && !rejection.config.url.contains("logerror"));
  3242. if (shouldHandle) {
  3243. var message = "An error occured :<br>" + rejection.status + ": " + rejection.statusText;
  3244.  
  3245. if (rejection.data) {
  3246. message += "<br><br>" + rejection.data;
  3247. }
  3248. GeneralModalService.open(message);
  3249.  
  3250. } else if (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && rejection.config.url.contains("logerror")) {
  3251. console.log("Not handling connection error while sending exception to server");
  3252. }
  3253.  
  3254. return $q.reject(rejection);
  3255. }
  3256. };
  3257. }]);
  3258.  
  3259.  
  3260. nzbhydraapp.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {
  3261. $httpProvider.interceptors.push('RequestsErrorHandler');
  3262.  
  3263. // --- Decorate $http to add a special header by default ---
  3264.  
  3265. function addHeaderToConfig(config) {
  3266. config = config || {};
  3267. config.headers = config.headers || {};
  3268.  
  3269. // Add the header unless user asked to handle errors himself
  3270. if (!specificallyHandleInProgress) {
  3271. config.headers[HEADER_NAME] = true;
  3272. }
  3273.  
  3274. return config;
  3275. }
  3276.  
  3277. // The rest here is mostly boilerplate needed to decorate $http safely
  3278. $provide.decorator('$http', ['$delegate', function ($delegate) {
  3279. function decorateRegularCall(method) {
  3280. return function (url, config) {
  3281. return $delegate[method](url, addHeaderToConfig(config));
  3282. };
  3283. }
  3284.  
  3285. function decorateDataCall(method) {
  3286. return function (url, data, config) {
  3287. return $delegate[method](url, data, addHeaderToConfig(config));
  3288. };
  3289. }
  3290.  
  3291. function copyNotOverriddenAttributes(newHttp) {
  3292. for (var attr in $delegate) {
  3293. if (!newHttp.hasOwnProperty(attr)) {
  3294. if (typeof($delegate[attr]) === 'function') {
  3295. newHttp[attr] = function () {
  3296. return $delegate.apply($delegate, arguments);
  3297. };
  3298. } else {
  3299. newHttp[attr] = $delegate[attr];
  3300. }
  3301. }
  3302. }
  3303. }
  3304.  
  3305. var newHttp = function (config) {
  3306. return $delegate(addHeaderToConfig(config));
  3307. };
  3308.  
  3309. newHttp.get = decorateRegularCall('get');
  3310. newHttp.delete = decorateRegularCall('delete');
  3311. newHttp.head = decorateRegularCall('head');
  3312. newHttp.jsonp = decorateRegularCall('jsonp');
  3313. newHttp.post = decorateDataCall('post');
  3314. newHttp.put = decorateDataCall('put');
  3315.  
  3316. copyNotOverriddenAttributes(newHttp);
  3317.  
  3318. return newHttp;
  3319. }]);
  3320. }]);
  3321. hashCode = function (s) {
  3322. return s.split("").reduce(function (a, b) {
  3323. a = ((a << 5) - a) + b.charCodeAt(0);
  3324. return a & a
  3325. }, 0);
  3326. };
  3327.  
  3328. angular
  3329. .module('nzbhydraApp').run(["formlyConfig", "formlyValidationMessages", function (formlyConfig, formlyValidationMessages) {
  3330. formlyValidationMessages.addStringMessage('required', 'This field is required');
  3331. formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'fc.$touched || form.$submitted';
  3332.  
  3333. }]);
  3334.  
  3335. angular
  3336. .module('nzbhydraApp')
  3337. .config(["formlyConfigProvider", function config(formlyConfigProvider) {
  3338. formlyConfigProvider.extras.removeChromeAutoComplete = true;
  3339. formlyConfigProvider.extras.explicitAsync = true;
  3340. formlyConfigProvider.disableWarnings = window.onProd;
  3341.  
  3342.  
  3343. formlyConfigProvider.setWrapper({
  3344. name: 'settingWrapper',
  3345. templateUrl: 'setting-wrapper.html'
  3346. });
  3347.  
  3348.  
  3349. formlyConfigProvider.setWrapper({
  3350. name: 'fieldset',
  3351. template: [
  3352. '<fieldset>',
  3353. '<legend>{{options.templateOptions.label}}</legend>',
  3354. '<formly-transclude></formly-transclude>',
  3355. '</fieldset>'
  3356. ].join(' ')
  3357. });
  3358.  
  3359. formlyConfigProvider.setType({
  3360. name: 'help',
  3361. template: [
  3362. '<div class="panel panel-default">',
  3363. '<div class="panel-body">',
  3364. '<div ng-repeat="line in options.templateOptions.lines">{{ line }}</div>',
  3365. '</div>',
  3366. '</div>'
  3367. ].join(' ')
  3368. });
  3369.  
  3370.  
  3371. formlyConfigProvider.setWrapper({
  3372. name: 'logicalGroup',
  3373. template: [
  3374. '<formly-transclude></formly-transclude>'
  3375. ].join(' ')
  3376. });
  3377.  
  3378. formlyConfigProvider.setType({
  3379. name: 'horizontalInput',
  3380. extends: 'input',
  3381. wrapper: ['settingWrapper', 'bootstrapHasError']
  3382. });
  3383.  
  3384. formlyConfigProvider.setType({
  3385. name: 'timeOfDay',
  3386. extends: 'horizontalInput',
  3387. controller: ['$scope', function ($scope) {
  3388. $scope.model[$scope.options.key] = moment.utc($scope.model[$scope.options.key]).toDate();
  3389. }]
  3390. });
  3391.  
  3392. formlyConfigProvider.setType({
  3393. name: 'percentInput',
  3394. template: [
  3395. '<input type="number" class="form-control" placeholder="Percent" ng-model="model[options.key]" ng-pattern="/^[0-9]+(\.[0-9]{1,2})?$/" step="0.01" required />'
  3396. ].join(' ')
  3397. });
  3398.  
  3399. formlyConfigProvider.setType({
  3400. name: 'apiKeyInput',
  3401. template: [
  3402. '<div class="input-group">',
  3403. '<input type="text" class="form-control" ng-model="model[options.key]"/>',
  3404. '<span class="input-group-btn input-group-btn2">',
  3405. '<button class="btn btn-default" type="button" ng-click="generate()"><span class="glyphicon glyphicon-refresh"></span></button>',
  3406. '</div>'
  3407. ].join(' '),
  3408. controller: function ($scope) {
  3409. $scope.generate = function () {
  3410. $scope.model[$scope.options.key] = (Math.random() * 1e32).toString(36);
  3411. }
  3412. }
  3413. });
  3414.  
  3415. formlyConfigProvider.setType({
  3416. name: 'testConnection',
  3417. templateUrl: 'button-test-connection.html'
  3418. });
  3419.  
  3420.  
  3421. formlyConfigProvider.setType({
  3422. name: 'horizontalTestConnection',
  3423. extends: 'testConnection',
  3424. wrapper: ['settingWrapper', 'bootstrapHasError']
  3425. });
  3426.  
  3427. formlyConfigProvider.setType({
  3428. name: 'checkCaps',
  3429. templateUrl: 'button-check-caps.html',
  3430. controller: function ($scope, ConfigBoxService) {
  3431. $scope.message = "";
  3432. $scope.uniqueId = hashCode($scope.model.name) + hashCode($scope.model.host);
  3433.  
  3434. var testButton = "#button-check-caps-" + $scope.uniqueId;
  3435. var testMessage = "#message-check-caps-" + $scope.uniqueId;
  3436.  
  3437. function showSuccess() {
  3438. angular.element(testButton).removeClass("btn-default");
  3439. angular.element(testButton).removeClass("btn-danger");
  3440. angular.element(testButton).addClass("btn-success");
  3441. }
  3442.  
  3443. function showError() {
  3444. angular.element(testButton).removeClass("btn-default");
  3445. angular.element(testButton).removeClass("btn-success");
  3446. angular.element(testButton).addClass("btn-danger");
  3447. }
  3448.  
  3449. $scope.checkCaps = function () {
  3450. angular.element(testButton).addClass("glyphicon-refresh-animate");
  3451.  
  3452. var url = "internalapi/test_caps";
  3453. var params = {indexer: $scope.model.name, apikey: $scope.model.apikey, host: $scope.model.host};
  3454. ConfigBoxService.checkCaps(url, params, $scope.model).then(function (data, model) {
  3455. angular.element(testMessage).text("Supports: " + data.supportedIds + "," ? data.supportedIds && data.supportedTypes : "" + data.supportedTypes);
  3456. showSuccess();
  3457. }, function (message) {
  3458. angular.element(testMessage).text(message);
  3459. showError();
  3460. }).finally(function () {
  3461. angular.element(testButton).removeClass("glyphicon-refresh-animate");
  3462. });
  3463. }
  3464. }
  3465. });
  3466.  
  3467. formlyConfigProvider.setType({
  3468. name: 'horizontalCheckCaps',
  3469. extends: 'checkCaps',
  3470. wrapper: ['settingWrapper', 'bootstrapHasError']
  3471. });
  3472.  
  3473.  
  3474. formlyConfigProvider.setType({
  3475. name: 'horizontalApiKeyInput',
  3476. extends: 'apiKeyInput',
  3477. wrapper: ['settingWrapper', 'bootstrapHasError']
  3478. });
  3479.  
  3480. formlyConfigProvider.setType({
  3481. name: 'horizontalPercentInput',
  3482. extends: 'percentInput',
  3483. wrapper: ['settingWrapper', 'bootstrapHasError']
  3484. });
  3485.  
  3486.  
  3487. formlyConfigProvider.setType({
  3488. name: 'switch',
  3489. template:
  3490. '<div style="text-align:left"><input bs-switch type="checkbox" ng-model="model[options.key]"/></div>'
  3491. });
  3492.  
  3493.  
  3494. formlyConfigProvider.setType({
  3495. name: 'duoSetting',
  3496. extends: 'input',
  3497. defaultOptions: {
  3498. className: 'col-md-9',
  3499. templateOptions: {
  3500. type: 'number',
  3501. noRow: true,
  3502. label: ''
  3503. }
  3504. }
  3505. });
  3506.  
  3507. formlyConfigProvider.setType({
  3508. name: 'horizontalSwitch',
  3509. extends: 'switch',
  3510. wrapper: ['settingWrapper', 'bootstrapHasError']
  3511. });
  3512.  
  3513. formlyConfigProvider.setType({
  3514. name: 'horizontalSelect',
  3515. extends: 'select',
  3516. wrapper: ['settingWrapper', 'bootstrapHasError']
  3517. });
  3518.  
  3519. formlyConfigProvider.setType({
  3520. name: 'horizontalMultiselect',
  3521. defaultOptions: {
  3522. templateOptions: {
  3523. optionsAttr: 'bs-options',
  3524. ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',
  3525. valueProp: 'id',
  3526. labelProp: 'label',
  3527. getPlaceholder: function() {return "";}
  3528. }
  3529. },
  3530. templateUrl: 'ui-select-multiple.html',
  3531. wrapper: ['settingWrapper', 'bootstrapHasError']
  3532. });
  3533.  
  3534.  
  3535. formlyConfigProvider.setType({
  3536. name: 'label',
  3537. template: '<label class="control-label">{{to.label}}</label>'
  3538. });
  3539.  
  3540. formlyConfigProvider.setType({
  3541. name: 'duolabel',
  3542. extends: 'label',
  3543. defaultOptions: {
  3544. className: 'col-md-2',
  3545. templateOptions: {
  3546. label: '-'
  3547. }
  3548. }
  3549. });
  3550.  
  3551. formlyConfigProvider.setType({
  3552. name: 'repeatSection',
  3553. templateUrl: 'repeatSection.html',
  3554. controller: function ($scope) {
  3555. $scope.formOptions = {formState: $scope.formState};
  3556. $scope.addNew = addNew;
  3557. $scope.remove = remove;
  3558. $scope.copyFields = copyFields;
  3559.  
  3560. function copyFields(fields) {
  3561. fields = angular.copy(fields);
  3562. $scope.repeatfields = fields;
  3563. return fields;
  3564. }
  3565.  
  3566. $scope.clear = function (field) {
  3567. return _.mapObject(field, function (key, val) {
  3568. if (typeof val === 'object') {
  3569. return $scope.clear(val);
  3570. }
  3571. return undefined;
  3572.  
  3573. });
  3574. };
  3575.  
  3576.  
  3577. function addNew() {
  3578. $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];
  3579. var repeatsection = $scope.model[$scope.options.key];
  3580. var newsection = angular.copy($scope.options.templateOptions.defaultModel);
  3581. repeatsection.push(newsection);
  3582. }
  3583.  
  3584. function remove($index) {
  3585. $scope.model[$scope.options.key].splice($index, 1);
  3586. }
  3587. }
  3588. });
  3589.  
  3590. formlyConfigProvider.setType({
  3591. name: 'arrayConfig',
  3592. templateUrl: 'arrayConfig.html',
  3593. controller: function ($scope, $uibModal) {
  3594. $scope.formOptions = {formState: $scope.formState};
  3595. $scope._showBox = _showBox;
  3596. $scope.showBox = showBox;
  3597. $scope.isInitial = false;
  3598.  
  3599. $scope.presets = $scope.options.data.presets;
  3600.  
  3601. function _showBox(model, parentModel, isInitial, callback) {
  3602. var modalInstance = $uibModal.open({
  3603. templateUrl: 'configBox.html',
  3604. controller: 'ConfigBoxInstanceController',
  3605. size: 'lg',
  3606. resolve: {
  3607. model: function () {
  3608. return model;
  3609. },
  3610. fields: function () {
  3611. return $scope.options.data.fieldsFunction(model, parentModel, isInitial, angular.injector());
  3612. },
  3613. isInitial: function () {
  3614. return isInitial
  3615. },
  3616. parentModel: function () {
  3617. return parentModel;
  3618. },
  3619. data: function () {
  3620. return $scope.options.data;
  3621. }
  3622. }
  3623. });
  3624.  
  3625.  
  3626. modalInstance.result.then(function () {
  3627. $scope.form.$setDirty(true);
  3628. if (angular.isDefined(callback)) {
  3629. callback(true);
  3630. }
  3631. }, function () {
  3632. if (angular.isDefined(callback)) {
  3633. callback(false);
  3634. }
  3635. });
  3636. }
  3637.  
  3638. function showBox(model, parentModel) {
  3639. $scope._showBox(model, parentModel, false)
  3640. }
  3641.  
  3642. $scope.addEntry = function (entriesCollection, preset) {
  3643. var model = angular.copy($scope.options.data.defaultModel);
  3644. if (angular.isDefined(preset)) {
  3645. _.extend(model, preset);
  3646. }
  3647.  
  3648. $scope.isInitial = true;
  3649.  
  3650. $scope._showBox(model, entriesCollection, true, function (isSubmitted) {
  3651. if (isSubmitted) {
  3652. entriesCollection.push(model);
  3653. }
  3654. });
  3655. };
  3656.  
  3657. }
  3658.  
  3659. });
  3660.  
  3661. }]);
  3662.  
  3663.  
  3664. angular.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) {
  3665.  
  3666. $scope.model = model;
  3667. $scope.fields = fields;
  3668. $scope.isInitial = isInitial;
  3669. $scope.allowDelete = data.allowDeleteFunction(model);
  3670. $scope.spinnerActive = false;
  3671. $scope.needsConnectionTest = false;
  3672.  
  3673. $scope.obSubmit = function () {
  3674. console.log($scope);
  3675. if ($scope.form.$valid) {
  3676.  
  3677. var a = data.checkBeforeClose($scope, model).then(function() {
  3678. $uibModalInstance.close($scope);
  3679. });
  3680. } else {
  3681. growl.error("Config invalid. Please check your settings.");
  3682. angular.forEach($scope.form.$error, function (error) {
  3683. angular.forEach(error, function (field) {
  3684. field.$setTouched();
  3685. });
  3686. });
  3687. }
  3688. };
  3689.  
  3690. $scope.reset = function () {
  3691. $scope.reset();
  3692. };
  3693.  
  3694. $scope.deleteEntry = function () {
  3695. parentModel.splice(parentModel.indexOf(model), 1);
  3696. $uibModalInstance.close($scope);
  3697. };
  3698.  
  3699. $scope.reset = function () {
  3700. if (angular.isDefined(data.resetFunction)) {
  3701. data.resetFunction($scope);
  3702. }
  3703. };
  3704.  
  3705. $scope.$on("modal.closing", function (targetScope, reason) {
  3706. if (reason == "backdrop click") {
  3707. $scope.reset($scope);
  3708. }
  3709. });
  3710. }]);
  3711.  
  3712. angular
  3713. .module('nzbhydraApp')
  3714. .factory('ConfigBoxService', ConfigBoxService);
  3715.  
  3716. function ConfigBoxService($http, $q) {
  3717.  
  3718. return {
  3719. checkConnection: checkConnection,
  3720. checkCaps: checkCaps
  3721. };
  3722.  
  3723. function checkConnection(url, settings) {
  3724. var deferred = $q.defer();
  3725.  
  3726. $http.post(url, settings).success(function (result) {
  3727. //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click
  3728. if (result.result) {
  3729. deferred.resolve();
  3730. } else {
  3731. deferred.reject({checked: true, message: result.message});
  3732. }
  3733. }).error(function (result) {
  3734. deferred.reject({checked: false, message: result.message});
  3735. });
  3736.  
  3737. return deferred.promise;
  3738. }
  3739.  
  3740. function checkCaps(url, params, model) {
  3741. var deferred = $q.defer();
  3742.  
  3743. $http.post(url, params).success(function (data) {
  3744. //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click
  3745. if (data.success) {
  3746. model.search_ids = data.supportedIds;
  3747. model.searchTypes = data.supportedTypes;
  3748. if (data.supportsAllCategories) { //Don't display all the categories, will be replaced with placeholder "All categories"
  3749. model.categories = [];
  3750. } else {
  3751. model.categories = data.supportedCategories;
  3752. }
  3753. model.animeCategory = data.animeCategory;
  3754. model.audiobookCategory = data.audiobookCategory;
  3755. model.comicCategory = data.comicCategory;
  3756. model.ebookCategory = data.ebookCategory;
  3757. model.magazineCategory = data.magazineCategory;
  3758. model.backend = data.backend;
  3759. deferred.resolve({supportedIds: data.supportedIds, supportedTypes: data.supportedTypes}, model);
  3760. } else {
  3761. deferred.reject(data.message);
  3762. }
  3763. }).error(function () {
  3764. deferred.reject("Unknown error");
  3765. });
  3766.  
  3767. return deferred.promise;
  3768. }
  3769.  
  3770. }
  3771. ConfigBoxService.$inject = ["$http", "$q"];
  3772.  
  3773.  
  3774.  
  3775.  
  3776.  
  3777. var filters = angular.module('filters', []);
  3778.  
  3779. filters.filter('bytes', function() {
  3780. return function(bytes) {
  3781. return filesize(bytes);
  3782. }
  3783. });
  3784.  
  3785. filters.filter('unsafe',
  3786. ["$sce", function ($sce) {
  3787. return function (value, type) {
  3788. return $sce.trustAs(type || 'html', text);
  3789. };
  3790. }]
  3791. );
  3792.  
  3793.  
  3794. angular
  3795. .module('nzbhydraApp')
  3796. .factory('DownloaderCategoriesService', DownloaderCategoriesService);
  3797.  
  3798. function DownloaderCategoriesService($http, $q, $uibModal) {
  3799.  
  3800. var categories = {};
  3801. var selectedCategory = {};
  3802.  
  3803. var service = {
  3804. get: getCategories,
  3805. invalidate: invalidate,
  3806. select: select,
  3807. openCategorySelection: openCategorySelection
  3808. };
  3809.  
  3810. var deferred;
  3811.  
  3812. return service;
  3813.  
  3814.  
  3815. function getCategories(downloader) {
  3816.  
  3817. function loadAll() {
  3818. if (angular.isDefined(categories) && angular.isDefined(categories.downloader)) {
  3819. var deferred = $q.defer();
  3820. deferred.resolve(categories.downloader);
  3821. return deferred.promise;
  3822. }
  3823.  
  3824. return $http.get('internalapi/getcategories', {params: {downloader: downloader.name}})
  3825. .then(function (categoriesResponse) {
  3826.  
  3827. console.log("Updating downloader categories cache");
  3828. var categories = {downloader: categoriesResponse.data.categories};
  3829. return categoriesResponse.data.categories;
  3830.  
  3831. }, function (error) {
  3832. throw error;
  3833. });
  3834. }
  3835.  
  3836. return loadAll().then(function (categories) {
  3837. return categories;
  3838. }, function (error) {
  3839. throw error;
  3840. });
  3841. }
  3842.  
  3843.  
  3844. function openCategorySelection(downloader) {
  3845. $uibModal.open({
  3846. templateUrl: 'static/html/directives/addable-nzb-modal.html',
  3847. controller: 'DownloaderCategorySelectionController',
  3848. size: "sm",
  3849. resolve: {
  3850. categories: function () {
  3851. return getCategories(downloader)
  3852. }
  3853. }
  3854. });
  3855. deferred = $q.defer();
  3856. return deferred.promise;
  3857. }
  3858.  
  3859. function select(category) {
  3860. selectedCategory = category;
  3861. console.log("Selected category " + category);
  3862. deferred.resolve(category);
  3863. }
  3864.  
  3865. function invalidate() {
  3866. console.log("Invalidating categories");
  3867. categories = undefined;
  3868. }
  3869. }
  3870. DownloaderCategoriesService.$inject = ["$http", "$q", "$uibModal"];
  3871.  
  3872. angular
  3873. .module('nzbhydraApp').controller('DownloaderCategorySelectionController', ["$scope", "$uibModalInstance", "DownloaderCategoriesService", "categories", function ($scope, $uibModalInstance, DownloaderCategoriesService, categories) {
  3874. console.log(categories);
  3875. $scope.categories = categories;
  3876. $scope.select = function (category) {
  3877. DownloaderCategoriesService.select(category);
  3878. $uibModalInstance.close($scope);
  3879. }
  3880. }]);
  3881. angular
  3882. .module('nzbhydraApp')
  3883. .controller('DownloadHistoryController', DownloadHistoryController);
  3884.  
  3885.  
  3886. function DownloadHistoryController($scope, StatsService, downloads) {
  3887. $scope.type = "All";
  3888. $scope.limit = 100;
  3889. $scope.pagination = {
  3890. current: 1
  3891. };
  3892.  
  3893. $scope.nzbDownloads = downloads.data.nzbDownloads;
  3894. $scope.totalDownloads = downloads.data.totalDownloads;
  3895.  
  3896. $scope.changeType = function (type) {
  3897. $scope.type = type;
  3898. getDownloadsPage($scope.pagination.current);
  3899. };
  3900.  
  3901.  
  3902. $scope.pageChanged = function (newPage) {
  3903. getDownloadsPage(newPage);
  3904. };
  3905.  
  3906. function getDownloadsPage(pageNumber) {
  3907. StatsService.getDownloadHistory(pageNumber, $scope.limit, $scope.type).then(function(downloads) {
  3908. $scope.nzbDownloads = downloads.data.nzbDownloads;
  3909. $scope.totalDownloads = downloads.data.totalDownloads;
  3910. });
  3911.  
  3912. }
  3913.  
  3914.  
  3915. }
  3916. DownloadHistoryController.$inject = ["$scope", "StatsService", "downloads"];
  3917.  
  3918. angular
  3919. .module('nzbhydraApp')
  3920. .factory('ConfigService', ConfigService);
  3921.  
  3922. function ConfigService($http, $q, $cacheFactory) {
  3923.  
  3924. var cache = $cacheFactory("nzbhydra");
  3925.  
  3926. return {
  3927. set: set,
  3928. get: get,
  3929. getSafe: getSafe,
  3930. invalidateSafe: invalidateSafe,
  3931. maySeeAdminArea: maySeeAdminArea
  3932. };
  3933.  
  3934.  
  3935. function set(newConfig) {
  3936. $http.put('internalapi/setsettings', newConfig)
  3937. .then(function (successresponse) {
  3938. console.log("Settings saved. Updating cache");
  3939. cache.put("config", newConfig);
  3940. invalidateSafe();
  3941. }, function (errorresponse) {
  3942. console.log("Error saving settings: " + errorresponse);
  3943. });
  3944. }
  3945.  
  3946. function get() {
  3947. var config = cache.get("config");
  3948. if (angular.isUndefined(config)) {
  3949. config = $http.get('internalapi/getconfig').then(function (data) {
  3950. return data.data;
  3951. });
  3952. cache.put("config", config);
  3953. }
  3954.  
  3955. return config;
  3956. }
  3957.  
  3958. function getSafe() {
  3959. var safeconfig = cache.get("safeconfig");
  3960. if (angular.isDefined(safeconfig)) {
  3961. return safeconfig;
  3962. }
  3963.  
  3964. return $http.get('internalapi/getsafeconfig').then(function (data) {
  3965. cache.put("safeconfig", data.data);
  3966. return data.data;
  3967. });
  3968.  
  3969.  
  3970. }
  3971.  
  3972. function invalidateSafe() {
  3973. cache.remove("safeconfig");
  3974. }
  3975.  
  3976. function maySeeAdminArea() {
  3977. function loadAll() {
  3978. var maySeeAdminArea = cache.get("maySeeAdminArea");
  3979. if (!angular.isUndefined(maySeeAdminArea)) {
  3980. var deferred = $q.defer();
  3981. deferred.resolve(maySeeAdminArea);
  3982. return deferred.promise;
  3983. }
  3984.  
  3985. return $http.get('internalapi/mayseeadminarea')
  3986. .then(function (configResponse) {
  3987. var config = configResponse.data;
  3988. cache.put("maySeeAdminArea", config);
  3989. return configResponse.data;
  3990. });
  3991. }
  3992.  
  3993. return loadAll().then(function (maySeeAdminArea) {
  3994. return maySeeAdminArea;
  3995. });
  3996. }
  3997. }
  3998. ConfigService.$inject = ["$http", "$q", "$cacheFactory"];
  3999. angular
  4000. .module('nzbhydraApp')
  4001. .factory('ConfigFields', ConfigFields);
  4002.  
  4003. function ConfigFields($injector) {
  4004.  
  4005. var restartWatcher;
  4006.  
  4007. return {
  4008. getFields: getFields,
  4009. setRestartWatcher: setRestartWatcher
  4010. };
  4011.  
  4012. function setRestartWatcher(restartWatcherFunction) {
  4013. restartWatcher = restartWatcherFunction;
  4014. }
  4015.  
  4016.  
  4017. function restartListener(field, newValue, oldValue) {
  4018. if (newValue != oldValue) {
  4019. restartWatcher();
  4020. }
  4021. }
  4022.  
  4023.  
  4024. function ipValidator() {
  4025. return {
  4026. expression: function ($viewValue, $modelValue) {
  4027. var value = $modelValue || $viewValue;
  4028. if (value) {
  4029. 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)
  4030. || /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value);
  4031. }
  4032. return true;
  4033. },
  4034. message: '$viewValue + " is not a valid IP Address"'
  4035. };
  4036. }
  4037.  
  4038. function regexValidator(regex, message, prefixViewValue) {
  4039. return {
  4040. expression: function ($viewValue, $modelValue) {
  4041. var value = $modelValue || $viewValue;
  4042. if (value) {
  4043. return regex.test(value);
  4044. }
  4045. return true;
  4046. },
  4047. message: (prefixViewValue ? '$viewValue + " ' : '" ') + message + '"'
  4048. };
  4049. }
  4050.  
  4051.  
  4052. function getCategoryFields() {
  4053. var fields = [];
  4054. var ConfigService = $injector.get("ConfigService");
  4055. var categories = ConfigService.getSafe().categories;
  4056. fields.push({
  4057. key: 'enableCategorySizes',
  4058. type: 'horizontalSwitch',
  4059. templateOptions: {
  4060. type: 'switch',
  4061. label: 'Category sizes',
  4062. help: "Preset min and max sizes depending on the selected category"
  4063. }
  4064. });
  4065. _.each(categories, function (category) {
  4066. if (category.name != "all" && category.name != "na") {
  4067. var categoryFields = [
  4068. {
  4069. key: "categories." + category.name + '.requiredWords',
  4070. type: 'horizontalInput',
  4071. templateOptions: {
  4072. type: 'text',
  4073. label: 'Required words',
  4074. placeholder: 'separate, with, commas, like, this'
  4075. }
  4076. },
  4077. {
  4078. key: "categories." + category.name + '.requiredRegex',
  4079. type: 'horizontalInput',
  4080. templateOptions: {
  4081. type: 'text',
  4082. label: 'Required regex',
  4083. help: 'Must be present in a title (case insensitive)'
  4084. }
  4085. },
  4086. {
  4087. key: "categories." + category.name + '.forbiddenWords',
  4088. type: 'horizontalInput',
  4089. templateOptions: {
  4090. type: 'text',
  4091. label: 'Forbidden words',
  4092. placeholder: 'separate, with, commas, like, this'
  4093. }
  4094. },
  4095. {
  4096. key: "categories." + category.name + '.forbiddenRegex',
  4097. type: 'horizontalInput',
  4098. templateOptions: {
  4099. type: 'text',
  4100. label: 'Forbidden regex',
  4101. help: 'Must not be present in a title (case insensitive)'
  4102. }
  4103. },
  4104. {
  4105. key: "categories." + category.name + '.applyRestrictions',
  4106. type: 'horizontalSelect',
  4107. templateOptions: {
  4108. label: 'Apply restrictions',
  4109. options: [
  4110. {name: 'Internal searches', value: 'internal'},
  4111. {name: 'API searches', value: 'external'},
  4112. {name: 'All searches', value: 'both'}
  4113. ],
  4114. help: "For which type of search word restrictions will be applied"
  4115. }
  4116. }
  4117. ];
  4118. categoryFields.push({
  4119. wrapper: 'settingWrapper',
  4120. templateOptions: {
  4121. label: 'Size preset'
  4122. },
  4123. fieldGroup: [
  4124. {
  4125. key: "categories." + category.name + '.min',
  4126. type: 'duoSetting',
  4127. templateOptions: {
  4128. addonRight: {
  4129. text: 'MB'
  4130. }
  4131. }
  4132. },
  4133. {
  4134. type: 'duolabel'
  4135. },
  4136. {
  4137. key: "categories." + category.name + '.max',
  4138. type: 'duoSetting', templateOptions: {addonRight: {text: 'MB'}}
  4139. }
  4140. ]
  4141. });
  4142. categoryFields.push({
  4143. key: "categories." + category.name + '.newznabCategories',
  4144. type: 'horizontalInput',
  4145. templateOptions: {
  4146. type: 'text',
  4147. label: 'Newznab categories',
  4148. help: 'Map newznab categories to hydra categories',
  4149. required: true
  4150. },
  4151. parsers: [function (value) {
  4152. if (!value) {
  4153. return value;
  4154. }
  4155. var arr = [];
  4156. arr.push.apply(arr, value.split(",").map(Number));
  4157. return arr;
  4158.  
  4159. }]
  4160. });
  4161. categoryFields.push({
  4162. key: "categories." + category.name + '.ignoreResults',
  4163. type: 'horizontalSelect',
  4164. templateOptions: {
  4165. label: 'Ignore results',
  4166. options: [
  4167. {name: 'For internal searches', value: 'internal'},
  4168. {name: 'For API searches', value: 'external'},
  4169. {name: 'Always', value: 'always'},
  4170. {name: 'Never', value: 'never'}
  4171. ],
  4172. help: "Ignore results from this category"
  4173. }
  4174. });
  4175.  
  4176. fields.push({
  4177. wrapper: 'fieldset',
  4178. templateOptions: {
  4179. label: category.pretty
  4180. },
  4181. fieldGroup: categoryFields
  4182.  
  4183. })
  4184. }
  4185. }
  4186. );
  4187. return fields;
  4188. }
  4189.  
  4190. function getFields(rootModel) {
  4191. return {
  4192. main: [
  4193. {
  4194. wrapper: 'fieldset',
  4195. templateOptions: {label: 'Hosting'},
  4196. fieldGroup: [
  4197. {
  4198. key: 'host',
  4199. type: 'horizontalInput',
  4200. templateOptions: {
  4201. type: 'text',
  4202. label: 'Host',
  4203. required: true,
  4204. placeholder: 'IPv4 address to bind to',
  4205. help: 'I strongly recommend using a reverse proxy instead of exposing this directly. Requires restart.'
  4206. },
  4207. validators: {
  4208. ipAddress: ipValidator()
  4209. },
  4210. watcher: {
  4211. listener: restartListener
  4212. }
  4213. },
  4214. {
  4215. key: 'port',
  4216. type: 'horizontalInput',
  4217. templateOptions: {
  4218. type: 'number',
  4219. label: 'Port',
  4220. required: true,
  4221. placeholder: '5050',
  4222. help: 'Requires restart'
  4223. },
  4224. validators: {
  4225. port: regexValidator(/^\d{1,5}$/, "is no valid port", true)
  4226. },
  4227. watcher: {
  4228. listener: restartListener
  4229. }
  4230. },
  4231. {
  4232. key: 'urlBase',
  4233. type: 'horizontalInput',
  4234. templateOptions: {
  4235. type: 'text',
  4236. label: 'URL base',
  4237. placeholder: '/nzbhydra',
  4238. help: 'Set when using an external proxy. Call using a trailing slash, e.g. http://www.domain.com/nzbhydra/'
  4239. },
  4240. validators: {
  4241. urlBase: regexValidator(/^\/[\w\/]*$/, "Base URL needs to start with a slash and must not end with one")
  4242. }
  4243. },
  4244. {
  4245. key: 'externalUrl',
  4246. type: 'horizontalInput',
  4247. templateOptions: {
  4248. type: 'text',
  4249. label: 'External URL',
  4250. placeholder: 'https://www.somedomain.com/nzbhydra/',
  4251. help: 'Set to the full external URL so machines outside can use the generated NZB links.'
  4252. }
  4253. },
  4254. {
  4255. key: 'useLocalUrlForApiAccess',
  4256. type: 'horizontalSwitch',
  4257. hideExpression: '!model.externalUrl',
  4258. templateOptions: {
  4259. type: 'switch',
  4260. label: 'Use local address in API results',
  4261. help: 'Disable to make API results use the external URL in NZB links.'
  4262. }
  4263. },
  4264. {
  4265. key: 'ssl',
  4266. type: 'horizontalSwitch',
  4267. templateOptions: {
  4268. type: 'switch',
  4269. label: 'Use SSL',
  4270. help: 'I recommend using a reverse proxy instead of this. Requires restart.'
  4271. },
  4272. watcher: {
  4273. listener: restartListener
  4274. }
  4275. },
  4276. {
  4277. key: 'socksProxy',
  4278. type: 'horizontalInput',
  4279. templateOptions: {
  4280. type: 'text',
  4281. label: 'SOCKS proxy',
  4282. placeholder: '127.0.0.1:1080',
  4283. help: "IPv4 only"
  4284. },
  4285. watcher: {
  4286. listener: restartListener
  4287. }
  4288. },
  4289. {
  4290. key: 'httpProxy',
  4291. type: 'horizontalInput',
  4292. templateOptions: {
  4293. type: 'text',
  4294. label: 'HTTP proxy',
  4295. placeholder: 'http://user:pass@10.0.0.1:1080',
  4296. help: "IPv4 only"
  4297. },
  4298. watcher: {
  4299. listener: restartListener
  4300. }
  4301. },
  4302. {
  4303. key: 'httpsProxy',
  4304. type: 'horizontalInput',
  4305. templateOptions: {
  4306. type: 'text',
  4307. label: 'HTTPS proxy',
  4308. placeholder: 'http://user:pass@10.0.0.1:1090',
  4309. help: "IPv4 only"
  4310. },
  4311. watcher: {
  4312. listener: restartListener
  4313. }
  4314. },
  4315. {
  4316. key: 'sslcert',
  4317. hideExpression: '!model.ssl',
  4318. type: 'horizontalInput',
  4319. templateOptions: {
  4320. type: 'text',
  4321. label: 'SSL certificate file',
  4322. required: true,
  4323. help: 'Requires restart.'
  4324. },
  4325. watcher: {
  4326. listener: restartListener
  4327. }
  4328. },
  4329. {
  4330. key: 'sslkey',
  4331. hideExpression: '!model.ssl',
  4332. type: 'horizontalInput',
  4333. templateOptions: {
  4334. type: 'text',
  4335. label: 'SSL key file',
  4336. required: true,
  4337. help: 'Requires restart.'
  4338. },
  4339. watcher: {
  4340. listener: restartListener
  4341. }
  4342. }
  4343.  
  4344. ]
  4345. },
  4346. {
  4347. wrapper: 'fieldset',
  4348. templateOptions: {label: 'UI'},
  4349. fieldGroup: [
  4350.  
  4351. {
  4352. key: 'theme',
  4353. type: 'horizontalSelect',
  4354. templateOptions: {
  4355. type: 'select',
  4356. label: 'Theme',
  4357. help: 'Reload page after saving',
  4358. options: [
  4359. {name: 'Default', value: 'default'},
  4360. {name: 'Dark', value: 'dark'}
  4361. ]
  4362. }
  4363. }
  4364. ]
  4365. },
  4366. {
  4367. wrapper: 'fieldset',
  4368. templateOptions: {label: 'Security'},
  4369. fieldGroup: [
  4370.  
  4371. {
  4372. key: 'apikey',
  4373. type: 'horizontalApiKeyInput',
  4374. templateOptions: {
  4375. label: 'API key',
  4376. help: 'Remove to disable. Alphanumeric only'
  4377. },
  4378. validators: {
  4379. apikey: regexValidator(/^[a-zA-Z0-9]*$/, "API key must only contain numbers and digits", false)
  4380. }
  4381. },
  4382. {
  4383. key: 'dereferer',
  4384. type: 'horizontalInput',
  4385. templateOptions: {
  4386. type: 'text',
  4387. label: 'Dereferer',
  4388. help: 'Redirect external links to hide your instance. Insert $s for target URL. Delete to disable.'
  4389. }
  4390. }
  4391. ]
  4392. },
  4393.  
  4394. {
  4395. wrapper: 'fieldset',
  4396. key: 'logging',
  4397. templateOptions: {label: 'Logging'},
  4398. fieldGroup: [
  4399. {
  4400. key: 'logfilelevel',
  4401. type: 'horizontalSelect',
  4402. templateOptions: {
  4403. type: 'select',
  4404. label: 'Logfile level',
  4405. options: [
  4406. {name: 'Critical', value: 'CRITICAL'},
  4407. {name: 'Error', value: 'ERROR'},
  4408. {name: 'Warning', value: 'WARNING'},
  4409. {name: 'Info', value: 'INFO'},
  4410. {name: 'Debug', value: 'DEBUG'}
  4411. ]
  4412. },
  4413. watcher: {
  4414. listener: restartListener
  4415. }
  4416. },
  4417. {
  4418. key: 'logfilename',
  4419. type: 'horizontalInput',
  4420. templateOptions: {
  4421. type: 'text',
  4422. label: 'Log file',
  4423. required: true
  4424. },
  4425. watcher: {
  4426. listener: restartListener
  4427. }
  4428. },
  4429. {
  4430. key: 'consolelevel',
  4431. type: 'horizontalSelect',
  4432. templateOptions: {
  4433. type: 'select',
  4434. label: 'Console log level',
  4435. options: [
  4436. {name: 'Critical', value: 'CRITICAL'},
  4437. {name: 'Error', value: 'ERROR'},
  4438. {name: 'Warning', value: 'WARNING'},
  4439. {name: 'Info', value: 'INFO'},
  4440. {name: 'Debug', value: 'DEBUG'}
  4441. ]
  4442. },
  4443. watcher: {
  4444. listener: restartListener
  4445. }
  4446. },
  4447. {
  4448. key: 'logIpAddresses',
  4449. type: 'horizontalSwitch',
  4450. templateOptions: {
  4451. type: 'switch',
  4452. label: 'Log IP addresses'
  4453. }
  4454. }
  4455.  
  4456.  
  4457. ]
  4458. },
  4459. {
  4460. wrapper: 'fieldset',
  4461. templateOptions: {label: 'Updating'},
  4462. fieldGroup: [
  4463.  
  4464. {
  4465. key: 'gitPath',
  4466. type: 'horizontalInput',
  4467. templateOptions: {
  4468. label: 'Git executable',
  4469. help: 'Set if git is not in your path'
  4470. }
  4471. },
  4472. {
  4473. key: 'branch',
  4474. type: 'horizontalInput',
  4475. templateOptions: {
  4476. type: 'text',
  4477. label: 'Repository branch',
  4478. required: true,
  4479. help: 'Stay on master. Seriously...'
  4480. }
  4481. }
  4482. ]
  4483. },
  4484.  
  4485. {
  4486. wrapper: 'fieldset',
  4487. templateOptions: {label: 'Other'},
  4488. fieldGroup: [
  4489. {
  4490. key: 'keepSearchResultsForDays',
  4491. type: 'horizontalInput',
  4492. templateOptions: {
  4493. type: 'number',
  4494. label: 'Store results for ...',
  4495. addonRight: {
  4496. text: 'days'
  4497. },
  4498. required: true,
  4499. help: 'Meta data from searches is stored in the database. When they\'re deleted links to hydra become invalid.'
  4500. }
  4501. },
  4502. {
  4503. key: 'debug',
  4504. type: 'horizontalSwitch',
  4505. templateOptions: {
  4506. type: 'switch',
  4507. label: 'Enable debugging',
  4508. help: "Only do this if you know what and why you're doing it"
  4509. }
  4510. },
  4511. {
  4512. key: 'runThreaded',
  4513. type: 'horizontalSwitch',
  4514. templateOptions: {
  4515. type: 'switch',
  4516. label: 'Run threaded server',
  4517. help: 'Requires restart'
  4518. },
  4519. watcher: {
  4520. listener: restartListener
  4521. }
  4522. },
  4523. {
  4524. key: 'startupBrowser',
  4525. type: 'horizontalSwitch',
  4526. templateOptions: {
  4527. type: 'switch',
  4528. label: 'Open browser on startup'
  4529. }
  4530. }
  4531. ]
  4532. }
  4533. ],
  4534.  
  4535. searching: [
  4536. {
  4537. wrapper: 'fieldset',
  4538. templateOptions: {
  4539. label: 'Indexer access'
  4540. },
  4541. fieldGroup: [
  4542. {
  4543. key: 'timeout',
  4544. type: 'horizontalInput',
  4545. templateOptions: {
  4546. type: 'number',
  4547. label: 'Timeout when accessing indexers',
  4548. addonRight: {
  4549. text: 'seconds'
  4550. }
  4551. }
  4552. },
  4553. {
  4554. key: 'ignoreTemporarilyDisabled',
  4555. type: 'horizontalSwitch',
  4556. templateOptions: {
  4557. type: 'switch',
  4558. label: 'Ignore temporarily disabled',
  4559. help: "If enabled access to indexers will never be paused after an error occurred"
  4560. }
  4561. },
  4562. {
  4563. key: 'ignorePassworded',
  4564. type: 'horizontalSwitch',
  4565. templateOptions: {
  4566. type: 'switch',
  4567. label: 'Ignore passworded releases',
  4568. help: "Not all indexers provide this information"
  4569. }
  4570. },
  4571. {
  4572. key: 'forbiddenWords',
  4573. type: 'horizontalInput',
  4574. templateOptions: {
  4575. type: 'text',
  4576. label: 'Forbidden words',
  4577. placeholder: 'separate, with, commas, like, this',
  4578. help: "Results with any of these words in the title will be ignored"
  4579. }
  4580. },
  4581. {
  4582. key: 'forbiddenRegex',
  4583. type: 'horizontalInput',
  4584. templateOptions: {
  4585. type: 'text',
  4586. label: 'Forbidden regex',
  4587. help: 'Must not be present in a title (case insensitive)'
  4588. }
  4589. },
  4590. {
  4591. key: 'requiredWords',
  4592. type: 'horizontalInput',
  4593. templateOptions: {
  4594. type: 'text',
  4595. label: 'Required words',
  4596. placeholder: 'separate, with, commas, like, this',
  4597. help: "Only results with at least one of these words in the title will be used"
  4598. }
  4599. },
  4600. {
  4601. key: 'requiredRegex',
  4602. type: 'horizontalInput',
  4603. templateOptions: {
  4604. type: 'text',
  4605. label: 'Required regex',
  4606. help: 'Must be present in a title (case insensitive)'
  4607. }
  4608. },
  4609. {
  4610. key: 'applyRestrictions',
  4611. type: 'horizontalSelect',
  4612. templateOptions: {
  4613. label: 'Apply restrictions',
  4614. options: [
  4615. {name: 'Internal searches', value: 'internal'},
  4616. {name: 'API searches', value: 'external'},
  4617. {name: 'All searches', value: 'both'}
  4618. ],
  4619. help: "For which type of search word restrictions will be applied"
  4620. }
  4621. },
  4622. {
  4623. key: 'maxAge',
  4624. type: 'horizontalInput',
  4625. templateOptions: {
  4626. type: 'number',
  4627. label: 'Maximum results age',
  4628. help: 'Results older than this are ignored. Can be overwritten per search.',
  4629. addonRight: {
  4630. text: 'days'
  4631. }
  4632. }
  4633. },
  4634. {
  4635. key: 'generate_queries',
  4636. type: 'horizontalMultiselect',
  4637. templateOptions: {
  4638. label: 'Generate queries',
  4639. options: [
  4640. {label: 'Internal searches', id: 'internal'},
  4641. {label: 'API searches', id: 'external'}
  4642. ],
  4643. help: "Generate queries for indexers which do not support ID based searches"
  4644. }
  4645. },
  4646. {
  4647. key: 'userAgent',
  4648. type: 'horizontalInput',
  4649. templateOptions: {
  4650. type: 'text',
  4651. label: 'User agent',
  4652. required: true
  4653. }
  4654. }
  4655.  
  4656. ]
  4657. },
  4658. {
  4659. wrapper: 'fieldset',
  4660. templateOptions: {
  4661. label: 'Result processing'
  4662. },
  4663. fieldGroup: [
  4664. {
  4665. key: 'htmlParser',
  4666. type: 'horizontalSelect',
  4667. templateOptions: {
  4668. type: 'select',
  4669. label: 'HTML parser',
  4670. options: [
  4671. {name: 'Default BS (slow)', value: 'html.parser'},
  4672. {name: 'LXML (faster, needs to be installed separately)', value: 'lxml'}
  4673. ]
  4674. }
  4675. },
  4676. {
  4677. key: 'duplicateSizeThresholdInPercent',
  4678. type: 'horizontalPercentInput',
  4679. templateOptions: {
  4680. type: 'text',
  4681. label: 'Duplicate size threshold',
  4682. required: true,
  4683. addonRight: {
  4684. text: '%'
  4685. }
  4686.  
  4687. }
  4688. },
  4689. {
  4690. key: 'duplicateAgeThreshold',
  4691. type: 'horizontalInput',
  4692. templateOptions: {
  4693. type: 'number',
  4694. label: 'Duplicate age threshold',
  4695. required: true,
  4696. addonRight: {
  4697. text: 'hours'
  4698. }
  4699. }
  4700. },
  4701. {
  4702. key: 'alwaysShowDuplicates',
  4703. type: 'horizontalSwitch',
  4704. templateOptions: {
  4705. type: 'switch',
  4706. label: 'Always show duplicates',
  4707. help: 'Activate to show duplicates in search results by default'
  4708. }
  4709. },
  4710. {
  4711. key: 'removeLanguage',
  4712. type: 'horizontalSwitch',
  4713. templateOptions: {
  4714. type: 'switch',
  4715. label: 'Remove language from newznab titles',
  4716. help: 'Some indexers add the language to the result title, preventing proper duplicate detection'
  4717. }
  4718. },
  4719. {
  4720. key: 'nzbAccessType',
  4721. type: 'horizontalSelect',
  4722. templateOptions: {
  4723. type: 'select',
  4724. label: 'NZB access type',
  4725. options: [
  4726. {name: 'Proxy NZBs from indexer', value: 'serve'},
  4727. {name: 'Redirect to the indexer', value: 'redirect'}
  4728. ],
  4729. help: "How access to NZBs is provided when NZBs are downloaded (by the user or external tools). Redirecting is recommended."
  4730. }
  4731. }
  4732. ]
  4733. }
  4734. ],
  4735.  
  4736. categories: getCategoryFields(),
  4737.  
  4738. downloaders: [
  4739. {
  4740. type: "arrayConfig",
  4741. data: {
  4742. defaultModel: {
  4743. enabled: true
  4744. },
  4745. entryTemplateUrl: 'downloaderEntry.html',
  4746. presets: getDownloaderPresets(),
  4747. presetsOnly: true,
  4748. addNewText: 'Add new downloader',
  4749. fieldsFunction: getDownloaderBoxFields,
  4750. allowDeleteFunction: function () {
  4751. return true;
  4752. },
  4753. checkBeforeClose: function (scope, model) {
  4754. var DownloaderCheckBeforeCloseService = $injector.get("DownloaderCheckBeforeCloseService");
  4755. return DownloaderCheckBeforeCloseService.check(scope, model);
  4756. },
  4757. resetFunction: function (scope) {
  4758. scope.options.resetModel();
  4759. scope.options.resetModel();
  4760. }
  4761.  
  4762. }
  4763. }
  4764. ],
  4765.  
  4766.  
  4767. indexers: [
  4768. {
  4769. type: "arrayConfig",
  4770. data: {
  4771. defaultModel: {
  4772. animeCategory: null,
  4773. comicCategory: null,
  4774. audiobookCategory: null,
  4775. magazineCategory: null,
  4776. ebookCategory: null,
  4777. enabled: true,
  4778. categories: [],
  4779. host: null,
  4780. apikey: null,
  4781. hitLimit: null,
  4782. hitLimitResetTime: 0,
  4783. timeout: null,
  4784. name: null,
  4785. showOnSearch: true,
  4786. score: 0,
  4787. username: null,
  4788. password: null,
  4789. preselect: true,
  4790. type: 'newznab',
  4791. accessType: "both",
  4792. search_ids: undefined, //["imdbid", "rid", "tvdbid"],
  4793. searchTypes: undefined, //["tvsearch", "movie"]
  4794. backend: 'newznab'
  4795. },
  4796. addNewText: 'Add new indexer',
  4797. entryTemplateUrl: 'indexerEntry.html',
  4798. presets: getIndexerPresets(),
  4799. fieldsFunction: getIndexerBoxFields,
  4800. allowDeleteFunction: function (model) {
  4801. return model.type == 'newznab' || model.type == 'jackett';
  4802. },
  4803. checkBeforeClose: function (scope, model) {
  4804. var IndexerCheckBeforeCloseService = $injector.get("IndexerCheckBeforeCloseService");
  4805. return IndexerCheckBeforeCloseService.check(scope, model);
  4806. },
  4807. resetFunction: function (scope) {
  4808. //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))
  4809. scope.options.resetModel();
  4810. scope.options.resetModel();
  4811. }
  4812.  
  4813. }
  4814. }
  4815. ],
  4816.  
  4817. auth: [
  4818. {
  4819. key: 'authType',
  4820. type: 'horizontalSelect',
  4821. templateOptions: {
  4822. label: 'Auth type',
  4823. options: [
  4824. {name: 'None', value: 'none'},
  4825. {name: 'HTTP Basic auth', value: 'basic'},
  4826. {name: 'Login form', value: 'form'}
  4827. ]
  4828.  
  4829. }
  4830. },
  4831. {
  4832. key: 'restrictSearch',
  4833. type: 'horizontalSwitch',
  4834. templateOptions: {
  4835. type: 'switch',
  4836. label: 'Restrict searching',
  4837. help: 'Restrict access to searching'
  4838. },
  4839. hideExpression: function () {
  4840. return rootModel.auth.authType == "none";
  4841. }
  4842. },
  4843. {
  4844. key: 'restrictStats',
  4845. type: 'horizontalSwitch',
  4846. templateOptions: {
  4847. type: 'switch',
  4848. label: 'Restrict stats',
  4849. help: 'Restrict access to stats'
  4850. },
  4851. hideExpression: function () {
  4852. return rootModel.auth.authType == "none";
  4853. }
  4854. },
  4855. {
  4856. key: 'restrictAdmin',
  4857. type: 'horizontalSwitch',
  4858. templateOptions: {
  4859. type: 'switch',
  4860. label: 'Restrict admin',
  4861. help: 'Restrict access to admin functions'
  4862. },
  4863. hideExpression: function () {
  4864. return rootModel.auth.authType == "none";
  4865. }
  4866. },
  4867. {
  4868. type: 'repeatSection',
  4869. key: 'users',
  4870. model: rootModel.auth,
  4871. templateOptions: {
  4872. btnText: 'Add new user',
  4873. altLegendText: 'Authless',
  4874. fields: [
  4875. {
  4876. key: 'username',
  4877. type: 'horizontalInput',
  4878. templateOptions: {
  4879. type: 'text',
  4880. label: 'Username',
  4881. required: true
  4882. }
  4883.  
  4884. },
  4885. {
  4886. key: 'password',
  4887. type: 'horizontalInput',
  4888. templateOptions: {
  4889. type: 'password',
  4890. label: 'Password',
  4891. required: true
  4892. }
  4893. },
  4894. {
  4895. key: 'maySeeAdmin',
  4896. type: 'horizontalSwitch',
  4897. templateOptions: {
  4898. type: 'switch',
  4899. label: 'May see admin area'
  4900. }
  4901. },
  4902. {
  4903. key: 'maySeeStats',
  4904. type: 'horizontalSwitch',
  4905. templateOptions: {
  4906. type: 'switch',
  4907. label: 'May see stats'
  4908. },
  4909. hideExpression: 'model.maySeeAdmin'
  4910. }
  4911.  
  4912. ],
  4913. defaultModel: {
  4914. username: null,
  4915. password: null,
  4916. maySeeStats: true,
  4917. maySeeAdmin: true
  4918. }
  4919. }
  4920. }
  4921. ]
  4922. };
  4923. }
  4924. }
  4925. ConfigFields.$inject = ["$injector"];
  4926.  
  4927.  
  4928. function getIndexerPresets() {
  4929. return [
  4930. [
  4931. {
  4932. name: "6box",
  4933. host: "https://6box.me"
  4934. },
  4935. {
  4936. name: "6box sptweb",
  4937. host: "https://6box.me/spotweb"
  4938. },
  4939. {
  4940. name: "DogNZB",
  4941. host: "https://api.dognzb.cr"
  4942. },
  4943. {
  4944. name: "Drunken Slug",
  4945. host: "https://drunkenslug.com"
  4946. },
  4947. {
  4948. name: "miatrix",
  4949. host: "https://www.miatrix.com"
  4950. },
  4951. {
  4952. name: "NZB Finder",
  4953. host: "https://nzbfinder.ws"
  4954. },
  4955. {
  4956. name: "NZBs.org",
  4957. host: "https://nzbs.org"
  4958. },
  4959. {
  4960. name: "nzb.is",
  4961. host: "https://nzb.is"
  4962. },
  4963. {
  4964. name: "nzb.su",
  4965. host: "https://api.nzb.su"
  4966. },
  4967. {
  4968. name: "NZBGeek",
  4969. host: "https://api.nzbgeek.info"
  4970. },
  4971. {
  4972. name: "NzbNdx",
  4973. host: "https://www.nzbndx.com"
  4974. },
  4975. {
  4976. name: "NzBNooB",
  4977. host: "https://www.nzbnoob.com"
  4978. },
  4979. {
  4980. name: "nzbplanet",
  4981. host: "https://nzbplanet.net"
  4982. },
  4983. {
  4984. name: "oznzb",
  4985. host: "https://api.oznzb.com/"
  4986. },
  4987. {
  4988. name: "omgwtfnzbs",
  4989. host: "https://api.omgwtfnzbs.me/"
  4990. },
  4991. {
  4992. name: "SimplyNZBs",
  4993. host: "https://simplynzbs.com"
  4994. }
  4995. ],
  4996. [
  4997. {
  4998. name: "Jackett/Cardigann",
  4999. host: "http://127.0.0.1:9117/torznab/YOURTRACKER",
  5000. search_ids: [],
  5001. searchTypes: [],
  5002. type: "jackett",
  5003. accessType: "internal"
  5004. }
  5005. ]
  5006. ];
  5007. }
  5008.  
  5009. function getIndexerBoxFields(model, parentModel, isInitial, injector) {
  5010. var fieldset = [];
  5011.  
  5012. fieldset.push({
  5013. key: 'enabled',
  5014. type: 'horizontalSwitch',
  5015. templateOptions: {
  5016. type: 'switch',
  5017. label: 'Enabled'
  5018. }
  5019. });
  5020.  
  5021. if (model.type == 'newznab' || model.type == 'jackett') {
  5022. fieldset.push(
  5023. {
  5024. key: 'name',
  5025. type: 'horizontalInput',
  5026. templateOptions: {
  5027. type: 'text',
  5028. label: 'Name',
  5029. required: true,
  5030. help: 'Used for identification. Changing the name will lose all history and stats!'
  5031. },
  5032. validators: {
  5033. uniqueName: {
  5034. expression: function (viewValue) {
  5035. if (isInitial || viewValue != model.name) {
  5036. return _.pluck(parentModel, "name").indexOf(viewValue) == -1;
  5037. }
  5038. return true;
  5039. },
  5040. message: '"Indexer \\"" + $viewValue + "\\" already exists"'
  5041. }
  5042. }
  5043. })
  5044. }
  5045. if (model.type == 'newznab' || model.type == 'jackett') {
  5046. fieldset.push(
  5047. {
  5048. key: 'host',
  5049. type: 'horizontalInput',
  5050. templateOptions: {
  5051. type: 'text',
  5052. label: 'Host',
  5053. required: true,
  5054. placeholder: 'http://www.someindexer.com'
  5055. },
  5056. watcher: {
  5057. listener: function (field, newValue, oldValue, scope) {
  5058. if (newValue != oldValue) {
  5059. scope.$parent.needsConnectionTest = true;
  5060. }
  5061. }
  5062. }
  5063. }
  5064. )
  5065. }
  5066.  
  5067. if (model.type == 'newznab' || model.type == 'jackett') {
  5068. fieldset.push(
  5069. {
  5070. key: 'apikey',
  5071. type: 'horizontalInput',
  5072. templateOptions: {
  5073. type: 'text',
  5074. required: true,
  5075. label: 'API Key'
  5076. },
  5077. watcher: {
  5078. listener: function (field, newValue, oldValue, scope) {
  5079. if (newValue != oldValue) {
  5080. scope.$parent.needsConnectionTest = true;
  5081. }
  5082. }
  5083. }
  5084. }
  5085. )
  5086. }
  5087.  
  5088. fieldset.push(
  5089. {
  5090. key: 'score',
  5091. type: 'horizontalInput',
  5092. templateOptions: {
  5093. type: 'number',
  5094. label: 'Priority',
  5095. required: true,
  5096. help: 'When duplicate search results are found the result from the indexer with the highest number will be selected'
  5097. }
  5098. });
  5099.  
  5100. fieldset.push(
  5101. {
  5102. key: 'timeout',
  5103. type: 'horizontalInput',
  5104. templateOptions: {
  5105. type: 'number',
  5106. label: 'Timeout',
  5107. help: 'Supercedes the general timeout in "Searching"'
  5108. }
  5109. });
  5110.  
  5111. if (model.type == 'newznab' || model.type == 'jackett') {
  5112. fieldset.push(
  5113. {
  5114. key: 'hitLimit',
  5115. type: 'horizontalInput',
  5116. templateOptions: {
  5117. type: 'number',
  5118. label: 'API hit limit',
  5119. help: 'Maximum number of API hits since "API hit reset time"'
  5120. }
  5121. }
  5122. );
  5123. fieldset.push(
  5124. {
  5125. key: 'hitLimitResetTime',
  5126. type: 'horizontalInput',
  5127. hideExpression: '!model.hitLimit',
  5128. templateOptions: {
  5129. type: 'number',
  5130. label: 'API hit reset time',
  5131. help: 'UTC hour of day at which the API hit counter is reset (0==24). Leave empty for a rolling reset counter'
  5132. },
  5133. validators: {
  5134. timeOfDay: {
  5135. expression: function ($viewValue, $modelValue) {
  5136. var value = $modelValue || $viewValue;
  5137. return value >= 0 && value <= 24;
  5138. },
  5139. message: '$viewValue + " is not a valid hour of day (0-24)"'
  5140. }
  5141.  
  5142. }
  5143. });
  5144. }
  5145. if (model.type == 'newznab') {
  5146. fieldset.push(
  5147. {
  5148. key: 'username',
  5149. type: 'horizontalInput',
  5150. templateOptions: {
  5151. type: 'text',
  5152. required: false,
  5153. label: 'Username',
  5154. help: 'Only needed if indexer requires HTTP auth for API access (rare)'
  5155. },
  5156. watcher: {
  5157. listener: function (field, newValue, oldValue, scope) {
  5158. if (newValue != oldValue) {
  5159. scope.$parent.needsConnectionTest = true;
  5160. }
  5161. }
  5162. }
  5163. }
  5164. );
  5165. }
  5166. if (model.type == 'newznab') {
  5167. fieldset.push(
  5168. {
  5169. key: 'password',
  5170. type: 'horizontalInput',
  5171. hideExpression: '!model.username',
  5172. templateOptions: {
  5173. type: 'text',
  5174. required: false,
  5175. label: 'Password',
  5176. help: 'Only needed if indexer requires HTTP auth for API access (rare)'
  5177. }
  5178. }
  5179. )
  5180.  
  5181. }
  5182.  
  5183.  
  5184. if (model.type != "womble") {
  5185. fieldset.push(
  5186. {
  5187. key: 'preselect',
  5188. type: 'horizontalSwitch',
  5189. hideExpression: 'model.accessType == "external"',
  5190. templateOptions: {
  5191. type: 'switch',
  5192. label: 'Preselect',
  5193. help: 'Preselect this indexer on the search page'
  5194. }
  5195. }
  5196. )
  5197. }
  5198. if (model.type != "womble" && model.type != "jackett") {
  5199. fieldset.push(
  5200. {
  5201. key: 'accessType',
  5202. type: 'horizontalSelect',
  5203. templateOptions: {
  5204. label: 'Enable for...',
  5205. options: [
  5206. {name: 'Internal searches only', value: 'internal'},
  5207. {name: 'API searches only', value: 'external'},
  5208. {name: 'Internal and API searches', value: 'both'}
  5209. ]
  5210. }
  5211. }
  5212. );
  5213. }
  5214. if (model.type != "womble" && model.type != "anizb") {
  5215. fieldset.push(
  5216. {
  5217. key: 'categories',
  5218. type: 'horizontalMultiselect',
  5219. templateOptions: {
  5220. label: 'Enable for...',
  5221. help: 'You can decide that this indexer should only be used for certain categories',
  5222. options: [
  5223. {
  5224. id: "movies",
  5225. label: "Movies"
  5226. },
  5227. {
  5228. id: "movieshd",
  5229. label: "Movies HD"
  5230. },
  5231. {
  5232. id: "moviessd",
  5233. label: "Movies SD"
  5234. },
  5235. {
  5236. id: "tv",
  5237. label: "TV"
  5238. },
  5239. {
  5240. id: "tvhd",
  5241. label: "TV HD"
  5242. },
  5243. {
  5244. id: "tvsd",
  5245. label: "TV SD"
  5246. },
  5247. {
  5248. id: "anime",
  5249. label: "Anime"
  5250. },
  5251. {
  5252. id: "audio",
  5253. label: "Audio"
  5254. },
  5255. {
  5256. id: "flac",
  5257. label: "Audio FLAC"
  5258. },
  5259. {
  5260. id: "mp3",
  5261. label: "Audio MP3"
  5262. },
  5263. {
  5264. id: "audiobook",
  5265. label: "Audiobook"
  5266. },
  5267. {
  5268. id: "console",
  5269. label: "Console"
  5270. },
  5271. {
  5272. id: "pc",
  5273. label: "PC"
  5274. },
  5275. {
  5276. id: "xxx",
  5277. label: "XXX"
  5278. },
  5279. {
  5280. id: "ebook",
  5281. label: "Ebook"
  5282. },
  5283. {
  5284. id: "comic",
  5285. label: "Comic"
  5286. }],
  5287. getPlaceholder: function () {
  5288. return "All categories";
  5289. }
  5290. }
  5291. }
  5292. )
  5293. }
  5294.  
  5295. if (model.type == 'newznab') {
  5296. fieldset.push(
  5297. {
  5298. key: 'search_ids',
  5299. type: 'horizontalMultiselect',
  5300. templateOptions: {
  5301. label: 'Search IDs',
  5302. options: [
  5303. {label: 'TVDB', id: 'tvdbid'},
  5304. {label: 'TVRage', id: 'rid'},
  5305. {label: 'IMDB', id: 'imdbid'},
  5306. {label: 'Trakt', id: 'traktid'},
  5307. {label: 'TVMaze', id: 'tvmazeid'},
  5308. {label: 'TMDB', id: 'tmdbid'}
  5309. ],
  5310. getPlaceholder: function (model) {
  5311. if (angular.isUndefined(model)) {
  5312. return "Unknown";
  5313. }
  5314. return "None";
  5315. }
  5316. }
  5317. }
  5318. );
  5319. }
  5320. if (model.type == 'newznab' || model.type == 'jackett') {
  5321. fieldset.push(
  5322. {
  5323. key: 'searchTypes',
  5324. type: 'horizontalMultiselect',
  5325. templateOptions: {
  5326. label: 'Search types',
  5327. options: [
  5328. {label: 'Movies', id: 'movie'},
  5329. {label: 'TV', id: 'tvsearch'},
  5330. {label: 'Ebooks', id: 'book'},
  5331. {label: 'Audio', id: 'audio'}
  5332. ],
  5333. getPlaceholder: function (model) {
  5334. if (angular.isUndefined(model)) {
  5335. return "Unknown";
  5336. }
  5337. return "None";
  5338. }
  5339. }
  5340. }
  5341. )
  5342. }
  5343.  
  5344. if (model.type == 'newznab' || model.type == 'jackett') {
  5345. fieldset.push(
  5346. {
  5347. type: 'horizontalCheckCaps',
  5348. hideExpression: '!model.host || !model.apikey || !model.name',
  5349. templateOptions: {
  5350. label: 'Check capabilities',
  5351. help: 'Find out what search types the indexer supports. Done automatically for new indexers.'
  5352. }
  5353. }
  5354. )
  5355. }
  5356.  
  5357. if (model.type == 'nzbindex') {
  5358. fieldset.push(
  5359. {
  5360. key: 'generalMinSize',
  5361. type: 'horizontalInput',
  5362. templateOptions: {
  5363. type: 'number',
  5364. label: 'Min size',
  5365. 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'
  5366. }
  5367. }
  5368. );
  5369. }
  5370.  
  5371. return fieldset;
  5372. }
  5373.  
  5374.  
  5375. function getDownloaderBoxFields(model, parentModel, isInitial) {
  5376. var fieldset = [];
  5377.  
  5378. fieldset = _.union(fieldset, [
  5379. {
  5380. key: 'enabled',
  5381. type: 'horizontalSwitch',
  5382. templateOptions: {
  5383. type: 'switch',
  5384. label: 'Enabled'
  5385. }
  5386. },
  5387. {
  5388. key: 'name',
  5389. type: 'horizontalInput',
  5390. templateOptions: {
  5391. type: 'text',
  5392. label: 'Name',
  5393. required: true
  5394. },
  5395. validators: {
  5396. uniqueName: {
  5397. expression: function (viewValue) {
  5398. if (isInitial || viewValue != model.name) {
  5399. return _.pluck(parentModel, "name").indexOf(viewValue) == -1;
  5400. }
  5401. return true;
  5402. },
  5403. message: '"Downloader \\"" + $viewValue + "\\" already exists"'
  5404. }
  5405. }
  5406.  
  5407. }]);
  5408.  
  5409. if (model.type == "nzbget") {
  5410. fieldset = _.union(fieldset, [{
  5411. key: 'host',
  5412. type: 'horizontalInput',
  5413. templateOptions: {
  5414. type: 'text',
  5415. label: 'Host',
  5416. required: true
  5417. },
  5418. watcher: {
  5419. listener: function (field, newValue, oldValue, scope) {
  5420. if (newValue != oldValue) {
  5421. scope.$parent.needsConnectionTest = true;
  5422. }
  5423. }
  5424. }
  5425.  
  5426. },
  5427. {
  5428. key: 'port',
  5429. type: 'horizontalInput',
  5430. templateOptions: {
  5431. type: 'number',
  5432. label: 'Port',
  5433. placeholder: '5050',
  5434. required: true
  5435. },
  5436. watcher: {
  5437. listener: function (field, newValue, oldValue, scope) {
  5438. if (newValue != oldValue) {
  5439. scope.$parent.needsConnectionTest = true;
  5440. }
  5441. }
  5442. }
  5443. }, {
  5444. key: 'ssl',
  5445. type: 'horizontalSwitch',
  5446. templateOptions: {
  5447. type: 'switch',
  5448. label: 'Use SSL'
  5449. }
  5450. }]);
  5451. } else if (model.type == "sabnzbd") {
  5452. fieldset.push({
  5453. key: 'url',
  5454. type: 'horizontalInput',
  5455. templateOptions: {
  5456. type: 'text',
  5457. label: 'URL',
  5458. required: true
  5459. },
  5460. watcher: {
  5461. listener: function (field, newValue, oldValue, scope) {
  5462. if (newValue != oldValue) {
  5463. scope.$parent.needsConnectionTest = true;
  5464. }
  5465. }
  5466. }
  5467. });
  5468. }
  5469. fieldset = _.union(fieldset, [
  5470. {
  5471. key: 'username',
  5472. type: 'horizontalInput',
  5473. templateOptions: {
  5474. type: 'text',
  5475. label: 'Username'
  5476. },
  5477. watcher: {
  5478. listener: function (field, newValue, oldValue, scope) {
  5479. if (newValue != oldValue) {
  5480. scope.$parent.needsConnectionTest = true;
  5481. }
  5482. }
  5483. }
  5484. },
  5485. {
  5486. key: 'password',
  5487. type: 'horizontalInput',
  5488. templateOptions: {
  5489. type: 'password',
  5490. label: 'Password'
  5491. },
  5492. watcher: {
  5493. listener: function (field, newValue, oldValue, scope) {
  5494. if (newValue != oldValue) {
  5495. scope.$parent.needsConnectionTest = true;
  5496. }
  5497. }
  5498. }
  5499. }
  5500. ]);
  5501.  
  5502.  
  5503. if (model.type == "sabnzbd") {
  5504. fieldset.push({
  5505. key: 'apikey',
  5506. type: 'horizontalInput',
  5507. templateOptions: {
  5508. type: 'text',
  5509. label: 'API Key'
  5510. },
  5511. watcher: {
  5512. listener: function (field, newValue, oldValue, scope) {
  5513. if (newValue != oldValue) {
  5514. scope.$parent.needsConnectionTest = true;
  5515. }
  5516. }
  5517. }
  5518. })
  5519. }
  5520.  
  5521. fieldset = _.union(fieldset, [
  5522. {
  5523. key: 'defaultCategory',
  5524. type: 'horizontalInput',
  5525. templateOptions: {
  5526. type: 'text',
  5527. label: 'Default category',
  5528. help: 'When adding NZBs this category will be used instead of asking for the category',
  5529. placeholder: 'Ask when downloading'
  5530. }
  5531. },
  5532. {
  5533. key: 'nzbaccesstype',
  5534. type: 'horizontalSelect',
  5535. templateOptions: {
  5536. type: 'select',
  5537. label: 'NZB access type',
  5538. options: [
  5539. {name: 'Proxy NZBs from indexer', value: 'serve'},
  5540. {name: 'Redirect to the indexer', value: 'redirect'}
  5541. ],
  5542. help: "How external access to NZBs is provided. Redirecting is recommended."
  5543. }
  5544. },
  5545. {
  5546. key: 'nzbAddingType',
  5547. type: 'horizontalSelect',
  5548. templateOptions: {
  5549. type: 'select',
  5550. label: 'NZB adding type',
  5551. options: [
  5552. {name: 'Send link', value: 'link'},
  5553. {name: 'Upload NZB', value: 'nzb'}
  5554. ],
  5555. help: "How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data"
  5556. }
  5557. },
  5558. {
  5559. key: 'iconCssClass',
  5560. type: 'horizontalInput',
  5561. templateOptions: {
  5562. type: 'text',
  5563. label: 'Icon CSS class',
  5564. help: 'Copy an icon name from http://fontawesome.io/examples/ (e.g. "film")',
  5565. placeholder: 'Default'
  5566. }
  5567. }
  5568. ]);
  5569.  
  5570. return fieldset;
  5571. }
  5572.  
  5573. function getDownloaderPresets() {
  5574. return [[
  5575. {
  5576. host: "127.0.0.1",
  5577. name: "NZBGet",
  5578. password: "tegbzn6789x",
  5579. port: 6789,
  5580. ssl: false,
  5581. type: "nzbget",
  5582. username: "nzbgetx",
  5583. nzbAddingType: "link",
  5584. nzbaccesstype: "redirect",
  5585. iconCssClass: "",
  5586. downloadType: "nzb"
  5587. },
  5588. {
  5589. url: "http://localhost:8086",
  5590. type: "sabnzbd",
  5591. name: "SABnzbd",
  5592. nzbAddingType: "link",
  5593. nzbaccesstype: "redirect",
  5594. iconCssClass: "",
  5595. downloadType: "nzb",
  5596. username: null,
  5597. password: null
  5598. }
  5599. ]];
  5600. }
  5601.  
  5602.  
  5603. function handleConnectionCheckFail(ModalService, data, model, whatFailed, deferred) {
  5604. var message;
  5605. var yesText;
  5606. if (data.checked) {
  5607. message = "The connection to the " + whatFailed + " failed: " + data.message + "<br>Do you want to add it anyway?";
  5608. yesText = "I know what I'm doing";
  5609. } else {
  5610. message = "The connection to the " + whatFailed + " could not be tested, sorry";
  5611. yesText = "I'll risk it";
  5612. }
  5613. ModalService.open("Connection check failed", message, {
  5614. yes: {
  5615. onYes: function () {
  5616. deferred.resolve();
  5617. },
  5618. text: yesText
  5619. },
  5620. no: {
  5621. onNo: function () {
  5622. model.enabled = false;
  5623. deferred.resolve();
  5624. },
  5625. text: "Add it, but disabled"
  5626. },
  5627. cancel: {
  5628. onCancel: function () {
  5629. deferred.reject();
  5630. },
  5631. text: "Aahh, let me try again"
  5632. }
  5633. });
  5634.  
  5635. }
  5636.  
  5637.  
  5638. angular
  5639. .module('nzbhydraApp')
  5640. .factory('IndexerCheckBeforeCloseService', IndexerCheckBeforeCloseService);
  5641.  
  5642. function IndexerCheckBeforeCloseService($q, ModalService, ConfigBoxService, blockUI, growl) {
  5643.  
  5644. return {
  5645. check: checkBeforeClose
  5646. };
  5647.  
  5648. function checkBeforeClose(scope, model) {
  5649. var deferred = $q.defer();
  5650. if (!scope.needsConnectionTest) {
  5651. checkCaps(scope, model).then(function () {
  5652. deferred.resolve();
  5653. }, function () {
  5654. deferred.reject();
  5655. });
  5656. } else {
  5657. blockUI.start("Testing connection...");
  5658. scope.spinnerActive = true;
  5659. var url = "internalapi/test_newznab";
  5660. var settings = {host: model.host, apikey: model.apikey};
  5661. ConfigBoxService.checkConnection(url, JSON.stringify(settings)).then(function () {
  5662. checkCaps(scope, model).then(function () {
  5663. blockUI.reset();
  5664. scope.spinnerActive = false;
  5665. growl.info("Connection to the indexer tested successfully");
  5666. deferred.resolve();
  5667. }, function () {
  5668. blockUI.reset();
  5669. scope.spinnerActive = false;
  5670. deferred.reject();
  5671. });
  5672. },
  5673. function (data) {
  5674. blockUI.reset();
  5675. handleConnectionCheckFail(ModalService, data, model, "indexer", deferred);
  5676. }).finally(function () {
  5677. scope.spinnerActive = false;
  5678. blockUI.reset();
  5679. });
  5680. }
  5681. return deferred.promise;
  5682.  
  5683. }
  5684.  
  5685. function checkCaps(scope, model) {
  5686. var deferred = $q.defer();
  5687. var url = "internalapi/test_caps";
  5688. var settings = {indexer: model.name, apikey: model.apikey, host: model.host};
  5689. if (angular.isUndefined(model.search_ids) || angular.isUndefined(model.searchTypes)) {
  5690.  
  5691. blockUI.start("New indexer found. Testing its capabilities. This may take a bit...");
  5692. ConfigBoxService.checkCaps(url, JSON.stringify(settings), model).then(
  5693. function (data, model) {
  5694. blockUI.reset();
  5695. scope.spinnerActive = false;
  5696. growl.info("Successfully tested capabilites of indexer");
  5697. deferred.resolve();
  5698. },
  5699. function () {
  5700. blockUI.reset();
  5701. scope.spinnerActive = false;
  5702. model.search_ids = [];
  5703. model.searchTypes = [];
  5704. 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.");
  5705. deferred.resolve();
  5706. }).finally(
  5707. function () {
  5708. scope.spinnerActive = false;
  5709. })
  5710. } else {
  5711. deferred.resolve();
  5712. }
  5713. return deferred.promise;
  5714.  
  5715. }
  5716. }
  5717. IndexerCheckBeforeCloseService.$inject = ["$q", "ModalService", "ConfigBoxService", "blockUI", "growl"];
  5718.  
  5719.  
  5720. angular
  5721. .module('nzbhydraApp')
  5722. .factory('DownloaderCheckBeforeCloseService', DownloaderCheckBeforeCloseService);
  5723.  
  5724. function DownloaderCheckBeforeCloseService($q, ConfigBoxService, growl, ModalService, blockUI) {
  5725.  
  5726. return {
  5727. check: checkBeforeClose
  5728. };
  5729.  
  5730. function checkBeforeClose(scope, model) {
  5731. var deferred = $q.defer();
  5732. if (!scope.isInitial && !scope.needsConnectionTest) {
  5733. deferred.resolve();
  5734. } else {
  5735. scope.spinnerActive = true;
  5736. blockUI.start("Testing connection...");
  5737. var url = "internalapi/test_downloader";
  5738. ConfigBoxService.checkConnection(url, JSON.stringify(model)).then(function () {
  5739. blockUI.reset();
  5740. scope.spinnerActive = false;
  5741. growl.info("Connection to the downloader tested successfully");
  5742. deferred.resolve();
  5743. },
  5744. function (data) {
  5745. blockUI.reset();
  5746. scope.spinnerActive = false;
  5747. handleConnectionCheckFail(ModalService, data, model, "downloader", deferred);
  5748. }).finally(function () {
  5749. scope.spinnerActive = false;
  5750. blockUI.reset();
  5751. });
  5752. }
  5753. return deferred.promise;
  5754. }
  5755.  
  5756. }
  5757. DownloaderCheckBeforeCloseService.$inject = ["$q", "ConfigBoxService", "growl", "ModalService", "blockUI"];
  5758. angular
  5759. .module('nzbhydraApp')
  5760. .factory('ConfigModel', function () {
  5761. return {};
  5762. });
  5763.  
  5764. angular
  5765. .module('nzbhydraApp')
  5766. .factory('ConfigWatcher', function () {
  5767. var $scope;
  5768.  
  5769. return {
  5770. watch: watch
  5771. };
  5772.  
  5773. function watch(scope) {
  5774. $scope = scope;
  5775. $scope.$watchGroup(["config.main.host"], function () {
  5776. }, true);
  5777. }
  5778. });
  5779.  
  5780.  
  5781. angular
  5782. .module('nzbhydraApp')
  5783. .controller('ConfigController', ConfigController);
  5784.  
  5785. function ConfigController($scope, $http, ConfigService, config, DownloaderCategoriesService, ConfigFields, ConfigModel, ModalService, RestartService, $state, growl, $rootScope) {
  5786. $scope.config = config;
  5787. $scope.submit = submit;
  5788. $scope.activeTab = undefined;
  5789.  
  5790. $scope.restartRequired = false;
  5791. $scope.ignoreSaveNeeded = false;
  5792.  
  5793. ConfigFields.setRestartWatcher(function () {
  5794. $scope.restartRequired = true;
  5795. });
  5796.  
  5797.  
  5798. function submit() {
  5799. if ($scope.form.$valid) {
  5800.  
  5801. ConfigService.set($scope.config);
  5802. ConfigService.invalidateSafe();
  5803. $scope.form.$setPristine();
  5804. DownloaderCategoriesService.invalidate();
  5805. if ($scope.restartRequired) {
  5806. ModalService.open("Restart required", "The changes you have made may require a restart to be effective.<br>Do you want to restart now?", {
  5807. yes: {
  5808. onYes: function () {
  5809. RestartService.restart();
  5810. }
  5811. },
  5812. no: {
  5813. onNo: function () {
  5814. $scope.restartRequired = false;
  5815. }
  5816. }
  5817. });
  5818. }
  5819. } else {
  5820. growl.error("Config invalid. Please check your settings.");
  5821.  
  5822. //Ridiculously hacky way to make the error messages appear
  5823. try {
  5824. if (angular.isDefined(form.$error.required)) {
  5825. _.each(form.$error.required, function (item) {
  5826. if (angular.isDefined(item.$error.required)) {
  5827. _.each(item.$error.required, function (item2) {
  5828. item2.$setTouched();
  5829. });
  5830. }
  5831. });
  5832. }
  5833. angular.forEach($scope.form.$error.required, function (field) {
  5834. field.$setTouched();
  5835. });
  5836. } catch(err) {
  5837. //
  5838. }
  5839.  
  5840. }
  5841. }
  5842.  
  5843. ConfigModel = config;
  5844.  
  5845. $scope.fields = ConfigFields.getFields($scope.config);
  5846.  
  5847. $scope.allTabs = [
  5848. {
  5849. active: false,
  5850. state: 'root.config',
  5851. name: 'Main',
  5852. model: ConfigModel.main,
  5853. fields: $scope.fields.main
  5854. },
  5855. {
  5856. active: false,
  5857. state: 'root.config.auth',
  5858. name: 'Authorization',
  5859. model: ConfigModel.auth,
  5860. fields: $scope.fields.auth
  5861. },
  5862. {
  5863. active: false,
  5864. state: 'root.config.searching',
  5865. name: 'Searching',
  5866. model: ConfigModel.searching,
  5867. fields: $scope.fields.searching
  5868. },
  5869. {
  5870. active: false,
  5871. state: 'root.config.categories',
  5872. name: 'Categories',
  5873. model: ConfigModel.categories,
  5874. fields: $scope.fields.categories
  5875. },
  5876. {
  5877. active: false,
  5878. state: 'root.config.downloader',
  5879. name: 'Downloaders',
  5880. model: ConfigModel.downloaders,
  5881. fields: $scope.fields.downloaders
  5882. },
  5883. {
  5884. active: false,
  5885. state: 'root.config.indexers',
  5886. name: 'Indexers',
  5887. model: ConfigModel.indexers,
  5888. fields: $scope.fields.indexers
  5889. }
  5890. ];
  5891.  
  5892. for (var i = 0; i < $scope.allTabs.length; i++) {
  5893. if ($state.is($scope.allTabs[i].state)) {
  5894. $scope.allTabs[i].active = true;
  5895. $scope.activeTab = $scope.allTabs[i];
  5896. }
  5897. }
  5898.  
  5899. $scope.isSavingNeeded = function () {
  5900. return $scope.form.$dirty && $scope.form.$valid && !$scope.ignoreSaveNeeded;
  5901. };
  5902.  
  5903. $scope.goToConfigState = function (index) {
  5904. $state.go($scope.allTabs[index].state);
  5905. $scope.activeTab = $scope.allTabs[index];
  5906. };
  5907.  
  5908. $scope.help = function() {
  5909. $http.get("internalapi/gethelp", {params: {id: $scope.activeTab.name}}).then(function(result) {
  5910. var html = '<span style="text-align: left;">' + result.data + "</span>";
  5911. ModalService.open($scope.activeTab.name + " - Help", html, {}, "lg");
  5912. },
  5913. function() {
  5914. growl.error("Error while loading help")
  5915. })
  5916. };
  5917.  
  5918. $scope.$on('$stateChangeStart',
  5919. function (event, toState, toParams, fromState, fromParams) {
  5920. if ($scope.isSavingNeeded()) {
  5921. event.preventDefault();
  5922. ModalService.open("Unsaved changed", "Do you want to save before leaving?", {
  5923. yes: {
  5924. onYes: function() {
  5925. $scope.submit();
  5926. $state.go(toState);
  5927. },
  5928. text: "Yes"
  5929. },
  5930. no: {
  5931. onNo: function () {
  5932. $scope.ignoreSaveNeeded = true;
  5933. $scope.ctrl.options.resetModel();
  5934. $state.go(toState);
  5935. },
  5936. text: "No"
  5937. },
  5938. cancel: {
  5939. onCancel: function () {
  5940. event.preventDefault();
  5941. },
  5942. text: "Cancel"
  5943. }
  5944. });
  5945. }
  5946. })
  5947. }
  5948. ConfigController.$inject = ["$scope", "$http", "ConfigService", "config", "DownloaderCategoriesService", "ConfigFields", "ConfigModel", "ModalService", "RestartService", "$state", "growl", "$rootScope"];
  5949.  
  5950.  
  5951.  
  5952. angular
  5953. .module('nzbhydraApp')
  5954. .factory('CategoriesService', CategoriesService);
  5955.  
  5956. function CategoriesService(ConfigService) {
  5957.  
  5958. return {
  5959. getByName: getByName,
  5960. getAll: getAll,
  5961. getDefault: getDefault
  5962. };
  5963.  
  5964.  
  5965. function getByName(name) {
  5966. for (var category in ConfigService.getSafe().categories) {
  5967. category = ConfigService.getSafe().categories[category];
  5968. if (category.name == name || category.pretty == name) {
  5969. return category;
  5970. }
  5971. }
  5972. }
  5973.  
  5974. function getAll() {
  5975. return ConfigService.getSafe().categories;
  5976. }
  5977.  
  5978. function getDefault() {
  5979. return getAll()[1];
  5980. }
  5981.  
  5982. }
  5983. CategoriesService.$inject = ["ConfigService"];
  5984. angular
  5985. .module('nzbhydraApp')
  5986. .factory('BackupService', BackupService);
  5987.  
  5988. function BackupService($http) {
  5989.  
  5990. return {
  5991. getBackupsList: getBackupsList,
  5992. restoreFromFile: restoreFromFile
  5993. };
  5994.  
  5995.  
  5996. function getBackupsList() {
  5997. return $http.get('internalapi/getbackups').then(function (data) {
  5998. return data.data.backups;
  5999. });
  6000. }
  6001.  
  6002. function restoreFromFile(filename) {
  6003. return $http.get('internalapi/restorefrombackupfile', {params:{filename: filename}}).then(function (response) {
  6004. return response;
  6005. });
  6006. }
  6007.  
  6008. }
  6009. BackupService.$inject = ["$http"];
  6010. //# sourceMappingURL=nzbhydra.js.map
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement