From 279b119f4c615cb3242ff9e19b08f45365b206cd Mon Sep 17 00:00:00 2001 From: theotherp Date: Fri, 20 Jan 2023 15:02:00 +0100 Subject: [PATCH] Update to Spring Boot 3, new h2, use graalvm, GH actions (#816) * Handle database recreation errors better * Handle different db usernames * Improve shutdown * Don't run startup problem detection in main thread * Fix stats calculation with h2 2 * Fix unit tests * Disable some tests not yet fixed (if ever) * Use SCRIPT TO to backup database as it verifies integrity * Only delete backups if a newer successful one exists * Fix error while analysing log file for OOM errors * Fix CSRF * Log Spring stuff on INFO * Add features for v5 to changelog * Don't try to remove windows tray icon cause it results in a hang * Fix notification history pagination * Fix shutdown behavior, prevent misleading error messages * Remove javax.annotation and .activation * Fix baseConfig.yml * First step to native build Compile runs, executable can be started, but has errors. More work needed. * First step to native build RUns under windows, HtmlUnit error, great progress! * Instantiate indexers and downloaders directly to make it work with native BeanFactory.createBean and .autowireBean do not work * Disable javascript in OpenPortChecker to fix charset error in native * Test Github Actions * Test Github Actions * Test Github Actions * Test Github Actions * Adapt wrappers to support native images * Update wrapper build to use docker containers clean folder structure Use v2 binaries for native support * Add build scripts, remove circleCi * Native build github actions * Don't write yaml, use in-memory db in native build mode * Upload native image artifact * Test self-hosted runner * Build on all three OSes * Build on all three OSes * Build on all three OSes * Build macos * Build windows * Build windows and macos * user proper paths for uploading artifacts * user proper paths for uploading artifacts * user proper paths for uploading artifacts * All three native images are built and uploaded But linux native not runnable under WSL, trying ubuntu 20.04 * Build static binary for linux * Make wrapper and update manager support both 4.x and 5.x, native and generic * Try building static image again * Commit wrapper binaries to LFS * Move files back to include folders * Adapt build-and-release.cmd to shell script * Prepare release via actions, add generic release * Prepare release via actions, add generic release * Update other package to 3.0.0 * Delete old docker files * Support dry run in release plugin and github action * Always use github token in release if available * Fix PhantomJS compile error, disable actions on push * release needs build * Show file structure on runner * Build on self-hosted windows runner * Disable graalvm watchdog so hopefully the windows build completes * Build on push, update dependencies * Build on windows * Determine release type from existance of files instead of json * Check version of executables before releasing * Scripts for test docker containers * Show system tray via wrapper, make main process headless * Native hints * Migrate JUnit 4 to 5 * Fix auth error when no internal api key was provided * Move integration tests * First step to system tests * Run system tests on github actions * Run system tests on github actions * Delete tests using old mockwebserver, use mockserver in system test * Delete tests using old mockwebserver, use mockserver in system test * Disable native image build to get to working systemtest faster * Specifically download coreLinux * Upload and download docker images * Save images in separate step * Fix docker port mapping syntax, keep docker images for one day only * Fix docker daemon syntax * Check out repo before running tests * Move config validation to separate classes * Start moving config classes to separate module * Finish moving config classes to separate module * Initialize test instance with sensible data * Push and use docker images using ghcr * Use custom health check for hydra docker container * Disable CSRF, use debug level for log file * Tests for debug infos * Use ${{ github.workspace }} * Use temp folder for hydradocker data * Write simple downloader web tests * Use /tmp as data folder * Test download of zipped NZBs * Build shared module before running tests * Load joptsimple resource bundle * Improve error page * Log spring security on debug level * Build new native image * Use new image * USe new reflect-config.json. Do I have to do this every time? * Reenable native build * Add reflection marker for native hints * Ensure all relevant classes have a reflection marker * Call TMDB api directly instead of using library * Use correct URL in mocked newznab results when in pipeline * Fix session error in IndexerUniquenessScoreSaver * Clean up code after migration to Java 17 * Backup tests * Remove spring profile from core dockerfile so that it can hopefully be loaded from env * GOOD STATE: Build native before doing tests Image is built, Two tests fail, but most succeed, yay * Test configuration of external tools * Run unit tests, upload test reports * Fix test being OS dependent (oops) * Fix test being OS dependent (oops) * Fix another OS dependent test, try other test report action * Create system test report * Try controlling native build by using "skipnativebuild" in the commit message * Controlling native build by using "skipnativebuild" in the commit message works, yay * Prepare entity TOs * Merge shared config and mapping modules * Merge shared entities and mapping modules * First simple test for history * Clean install for native image build * Commit missing files * More tests for search history * Disable repo.spring.io because central repos should be preferred * Test download history * Remove repo.spring.io repo * Upload settings.xml for debugging * Try custom settings.xml to prevent using repo.spring.io skipnativebuild * Remove settings.xml stuff, try running *arr dockers directly skipnativebuild * Fix sonar host * Don't skip prepareArtifacts as other jobs depend on it * Run all containers manually skipnativebuild * Set spring profiles in test env again skipnativebuild * SEMI_GOOD_STEP USe custom docker network in system test Communication between containers works, still bytecode error and BackupData#getBackupFile not working skipnativebuild * Remove lazy loading in SearchResultEntity, use TransactionTemplate in IndexerUniquenessScoreSaver.java * Remove lazy loading in IndexerApiAccessEntity.java * Make entities final to perhaps prevent lazy loading? * Remove ToStringSerializer for entity IDs * Support TVMAZE ID in search history * Remove names from docker run commands * Try using docker host network * Try systemtest network again skipnativebuild * Print build version and timestamp at startup * GOOD_STEP Run mockserver first so core can access it Only two failing tests remain * Print version and timestamp even if in docker * Use mapped volume for black hole * YESSSS GREEN PIPELINE Use a category for *arr which is supported by a configured indexer * Add stats test, is kinda flakey * Remove integration tests * Install java in docker container for db migration * First step to migration test * Remove migration module from mvn call * Generate custom assertions for mapping module * Add caps check test * Add media info test * Add news test * Add notifications test * Reenable migration test, perhaps it works now? * Add missing assertion files * Try different docker sock, add unmarshaller resource bundle * Don't trim stacktraces in maven, log unparsable indexer output * Use hopefully correct migration tmp folder, create it before using * Use different md to html renderer * Make caps inherit XML * Fix news test, create /tmp/data/v1MigrationDataFolder in action * Try different temp folder skipnativebuild * Print version earlier * Run migration docker directly * Print version differently * Wait for healthy containers * Print version and timestamp from properties * Wait for healthy containers * Use latest docker image when skipping nativebuild skipnativebuild * Give containers a bit of time skipnativebuild * Use docker-compose skipnativebuild * Try java from path if executable not found * Try to get more current artifact * ALL GREEN YAAAAAAY Build image in first job if configured, then just use latest in system test * Precheck that wrapper executables are newer than source files * Run all for core and migration, respectively * Remove discord bot, publish via maven plugin when releasing * Use container name in external hydra url * Move database migration to spring component * Use SLF4J in system tests skipnativebuild * Use 5076 on container skipnativebuild * ONE LEFT Wait a bit between file mod date changes Only blackhole test not working anymore * Use name of test component (core, v1Migration) in black hole folder * Log properties and docker mounts skipnativebuild * ALL GREEN WITH two test runs Mount blackhole folder for core skipnativebuild * Check changes files to decide if native image is built * Check changes files to decide if mock server is built * Send build decision messages to notice * Use changelog.yaml instead of .json * Fix startup of wrapper without internal api key * Use new wrapperHashes2.json * Write messages for discord and wiki * Ensure database integrity before creating backup * Show warning to user if automatic backup has failed * Show better error message when user triggered backup creation failed * Shoe proper error message when debug infos creation / download failed * Update wrapper hashes * Add loadtest, cache OKHTTP clients * Improve performance of search result parsing by getting rid of regex * Compress native image binary * Update release script * Update release script * Check for environment variables in release script * Check for untracked files or uncommitted changes in release script * git ignore token files * Read tokens from files * Color output for release script * Remove call from release script (only needed on windows) * Call maven in batch mode * Build shared module first * Change compiled modules in release script * Ask me to run mvn in windows * Try building on self hosted runner again * Run on all self-hosted * Run on all self-hosted * Run on all self-hosted * Run on all self-hosted * Translate build script to powershell * Make maven quiet * Escape -d options in maven * Check exit code differently * Ignore afile.txt * Escape -D * Allow "wet run" without release * Add missing changelog.yaml * Prepare for release * Don't build shared in buildCore.cmd * Wait for linux version after check of others * Disable commit for test * Build core from main folder * Copy windows build artifacts to include folder * Make wrapper open browser if main process can't * Delete release shell script * Fix build in system-test.yml * Update changelog * Update some libraries, ignore snakeyaml exploit in snyk * Remove system test data * Update to Spring Boot 3.0.2 * Differentiate versions of spring boot and devtools * Set githubReleasesUrl directly --- .circleci/config.yml | 59 - .github/workflows/buildNative.yml | 112 + .github/workflows/release.yml | 57 + .github/workflows/system-test.yml | 130 + .github/workflows/test.yml | 19 + .gitignore | 2 + .idea/compiler.xml | 5 +- .idea/misc.xml | 3 +- .run/NzbHydraNativeEntrypoint.run.xml | 19 + .snyk | 9 + buildCore.cmd | 12 + changelog.md | 26 +- core/.gitignore | 3 +- core/buildCoreAotOnly.cmd | 12 + core/pom.xml | 260 +- core/runTracingAgent.cmd | 13 + .../main/java/org/nzbhydra/DevEndpoint.java | 18 +- .../main/java/org/nzbhydra/ExceptionInfo.java | 2 + .../java/org/nzbhydra/InstanceCounter.java | 7 +- core/src/main/java/org/nzbhydra/Markdown.java | 13 +- .../main/java/org/nzbhydra/NativeHints.java | 69 + core/src/main/java/org/nzbhydra/NzbHydra.java | 180 +- .../nzbhydra/NzbHydraNativeEntrypoint.java | 48 + .../java/org/nzbhydra/WindowsTrayIcon.java | 123 - .../java/org/nzbhydra/api/CapsGenerator.java | 13 +- .../org/nzbhydra/api/CategoryConverter.java | 5 +- .../java/org/nzbhydra/api/ExternalApi.java | 16 +- .../nzbhydra/api/NewznabJsonTransformer.java | 2 +- .../nzbhydra/api/NewznabXmlTransformer.java | 2 +- .../nzbhydra/api/stats/ApiStatsRequest.java | 2 + .../org/nzbhydra/auth/AsyncSupportFilter.java | 8 +- .../auth/AuthAndAccessEventHandler.java | 8 +- .../main/java/org/nzbhydra/auth/AuthWeb.java | 2 +- .../auth/ForwardedForRecognizingFilter.java | 8 +- .../auth/HeaderAuthenticationFilter.java | 33 +- .../HydraAnonymousAuthenticationFilter.java | 16 +- .../auth/HydraEmbeddedServletContainer.java | 5 +- ...ydraGlobalMethodSecurityConfiguration.java | 8 +- .../auth/HydraWebAuthenticationDetails.java | 6 +- .../auth/LoginAndAccessAttemptService.java | 2 +- .../nzbhydra/auth/PersistentLoginsEntity.java | 14 +- .../org/nzbhydra/auth/SecurityConfig.java | 157 +- .../org/nzbhydra/backup/BackupAndRestore.java | 83 +- .../java/org/nzbhydra/backup/BackupData.java | 11 +- .../java/org/nzbhydra/backup/BackupTask.java | 6 +- .../java/org/nzbhydra/backup/BackupWeb.java | 5 +- .../org/nzbhydra/backup/FailedBackupData.java | 45 + .../java/org/nzbhydra/config/BaseConfig.java | 306 - .../nzbhydra/config/BaseConfigHandler.java | 147 + .../org/nzbhydra/config/ConfigProvider.java | 3 + .../nzbhydra/config/ConfigReaderWriter.java | 32 +- .../java/org/nzbhydra/config/ConfigWeb.java | 25 +- .../nzbhydra/config/FileSystemBrowser.java | 4 + .../org/nzbhydra/config/LoggingConfig.java | 67 - .../java/org/nzbhydra/config/MainConfig.java | 243 - .../nzbhydra/config/NotificationConfig.java | 70 - .../config/NotificationConfigEntry.java | 33 - .../config/SearchSourceRestriction.java | 30 - .../org/nzbhydra/config/SearchingConfig.java | 168 - .../org/nzbhydra/config/ValidatingConfig.java | 108 - .../org/nzbhydra/config/auth/AuthConfig.java | 118 - .../ConfigMigrationStep006to007.java | 1 - .../ConfigMigrationStep007to008.java | 1 - .../ConfigMigrationStep008to009.java | 1 - .../ConfigMigrationStep009to010.java | 1 - .../ConfigMigrationStep010to011.java | 1 - .../ConfigMigrationStep011to012.java | 1 - .../ConfigMigrationStep012to013.java | 1 - .../ConfigMigrationStep013to014.java | 1 - .../ConfigMigrationStep018to019.java | 1 - .../config/safeconfig/SafeCategory.java | 2 + .../config/safeconfig/SafeConfig.java | 2 + .../safeconfig/SafeDownloaderConfig.java | 2 + .../config/safeconfig/SafeIndexerConfig.java | 2 + .../SensitiveDataHidingSerializer.java | 3 +- .../validation/AuthConfigValidator.java | 92 + .../validation/BaseConfigValidator.java | 172 + .../CategoriesConfigValidator.java} | 79 +- .../validation/ConfigValidationResult.java | 46 + .../validation/ConfigValidationTools.java | 73 + .../config/validation/ConfigValidator.java | 55 + .../DownloaderConfigValidator.java} | 72 +- .../DownloadingConfigValidator.java} | 88 +- .../validation/IndexerConfigValidator.java | 79 + .../validation/LoggingConfigValidator.java | 63 + .../validation/MainConfigValidator.java | 139 + .../NotificationConfigValidator.java | 52 + .../validation/SearchingConfigValidator.java | 102 + .../validation/UserAuthConfigValidator.java | 51 + .../nzbhydra/database/DatabaseRecreation.java | 284 +- .../database/DatabaseRecreationBean.java | 33 + .../database/DatabaseRecreationConfig.java | 43 + .../nzbhydra/database/FlywayMigration.java | 79 - .../nzbhydra/database/H2DialectExtended.java | 69 + .../migration/V2__MOVE_GENERIC_STORAGE.java | 50 - .../migration/V3__MOVE_GENERIC_STORAGE.java | 50 - .../debuginfos/DebugInfosProvider.java | 44 +- .../nzbhydra/debuginfos/DebugInfosWeb.java | 14 +- .../nzbhydra/downloading/DownloadResult.java | 6 +- .../downloading/FileDownloadEntity.java | 24 +- .../downloading/FileDownloadEvent.java | 4 - .../org/nzbhydra/downloading/FileHandler.java | 16 +- .../IndexerUniquenessScoreSaver.java | 55 +- .../downloaders/ConnectionCheckResult.java | 2 + .../downloading/downloaders/Downloader.java | 35 +- .../downloaders/DownloaderEntry.java | 2 + .../downloaders/DownloaderInstatiator.java | 64 + .../downloaders/DownloaderProvider.java | 16 +- .../DownloaderStatusRetrieval.java | 7 +- .../downloaders/DownloaderWebSocket.java | 6 +- .../downloaders/nzbget/NzbGet.java | 22 +- .../downloaders/sabnzbd/Sabnzbd.java | 26 +- .../downloading/nzbs/NzbHandlingWeb.java | 2 +- .../torrents/SaveOrSendTorrentsResponse.java | 2 + .../torrents/TorrentFileHandler.java | 12 +- .../torrents/TorrentHandlingWeb.java | 2 +- .../nzbhydra/externaltools/AddDialogInfo.java | 2 + .../nzbhydra/externaltools/ExternalTools.java | 22 +- .../java/org/nzbhydra/fortests/DebugWeb.java | 2 +- .../nzbhydra/fortests/NewznabItemData.java | 2 + .../fortests/NewznabResponseData.java | 2 + .../genericstorage/GenericStorage.java | 7 +- .../genericstorage/GenericStorageWeb.java | 7 +- .../org/nzbhydra/historystats/History.java | 36 +- .../org/nzbhydra/historystats/HistoryWeb.java | 43 +- .../java/org/nzbhydra/historystats/Stats.java | 146 +- .../nzbhydra/historystats/StatsResponse.java | 42 - .../historystats/stats/CountPerDayOfWeek.java | 21 - .../java/org/nzbhydra/indexers/Anizb.java | 25 +- .../java/org/nzbhydra/indexers/Binsearch.java | 68 +- .../org/nzbhydra/indexers/DevIndexer.java | 20 +- .../java/org/nzbhydra/indexers/DogNzb.java | 22 +- .../java/org/nzbhydra/indexers/Indexer.java | 102 +- .../indexers/IndexerApiAccessEntity.java | 19 +- .../indexers/IndexerApiAccessEntityShort.java | 16 +- .../indexers/IndexerApiAccessRepository.java | 11 +- .../org/nzbhydra/indexers/IndexerEntity.java | 15 +- .../indexers/IndexerHandlingStrategy.java | 4 +- .../indexers/IndexerSearchEntity.java | 19 +- .../indexers/IndexerStatusesCleanupTask.java | 7 +- .../nzbhydra/indexers/IndexerWebAccess.java | 16 +- .../java/org/nzbhydra/indexers/Newznab.java | 61 +- .../java/org/nzbhydra/indexers/NfoResult.java | 2 + .../java/org/nzbhydra/indexers/NzbGeek.java | 22 +- .../java/org/nzbhydra/indexers/NzbIndex.java | 24 +- .../org/nzbhydra/indexers/QueryGenerator.java | 16 +- .../indexers/capscheck/IndexerChecker.java | 19 +- .../indexers/capscheck/IndexerWeb.java | 5 + .../capscheck/JacketConfigRetriever.java | 8 +- .../indexers/status/IndexerLimit.java | 18 +- .../status/IndexerStatusesAndLimits.java | 8 +- .../nzbhydra/indexers/torznab/Torznab.java | 51 +- .../org/nzbhydra/logging/ColorConverter.java | 2 +- .../org/nzbhydra/logging/EceptionFilter.java | 2 +- .../org/nzbhydra/logging/LogAnonymizer.java | 6 +- .../nzbhydra/logging/LogContentProvider.java | 39 +- .../nzbhydra/logging/LoggingMarkerFilter.java | 2 +- .../logging/MdcThreadPoolExecutor.java | 9 +- .../org/nzbhydra/logging/ProgressLogger.java | 3 +- .../logging/ReversedLinesFileReader.java | 12 +- .../AutocompleteType.java} | 11 +- .../org/nzbhydra/mediainfo/CustomTmdb.java | 77 - .../org/nzbhydra/mediainfo/InfoProvider.java | 12 +- .../org/nzbhydra/mediainfo/MediaInfo.java | 27 +- .../org/nzbhydra/mediainfo/MediaInfoWeb.java | 86 +- .../org/nzbhydra/mediainfo/MovieInfo.java | 12 +- .../org/nzbhydra/mediainfo/TmdbHandler.java | 152 +- .../nzbhydra/mediainfo/TmdbSearchResult.java | 2 + .../java/org/nzbhydra/mediainfo/TvInfo.java | 13 +- .../org/nzbhydra/mediainfo/TvMazeHandler.java | 33 +- .../mediainfo/TvMazeSearchResult.java | 2 + .../migration/configmapping/Auth.java | 2 + .../migration/configmapping/Categories.java | 2 + .../migration/configmapping/Category.java | 2 + .../migration/configmapping/Downloader.java | 2 + .../migration/configmapping/Indexer.java | 2 + .../ListOrStringToStringDeserializer.java | 3 +- .../migration/configmapping/Logging.java | 2 + .../migration/configmapping/Main.java | 2 + .../migration/configmapping/OldConfig.java | 2 + .../migration/configmapping/Searching.java | 2 + .../migration/configmapping/User.java | 2 + .../org/nzbhydra/misc/OpenPortChecker.java | 6 +- .../main/java/org/nzbhydra/misc/WebHooks.java | 6 +- .../java/org/nzbhydra/news/NewsProvider.java | 4 +- .../main/java/org/nzbhydra/news/NewsWeb.java | 17 +- .../java/org/nzbhydra/news/ShownNews.java | 13 +- .../AuthFailureNotificationEvent.java | 3 + .../DownloadCompletionNotificationEvent.java | 3 + .../DownloadNotificationEvent.java | 3 + .../IndexerDisabledNotificationEvent.java | 3 + .../IndexerReenabledNotificationEvent.java | 3 + .../IndexerVipExpiryNotificationEvent.java | 3 + .../notifications/NotificationEntity.java | 34 +- .../notifications/NotificationEvent.java | 2 + .../notifications/NotificationHandler.java | 9 +- .../notifications/NotificationRepository.java | 1 + .../notifications/NotificationsWeb.java | 11 +- .../UpdateNotificationEvent.java | 3 + .../DeleteOldDatabaseBackupDetector.java | 56 + .../problemdetection/OutOfMemoryDetector.java | 50 +- .../OutdatedWrapperDetector.java | 22 +- .../problemdetection/ProblemDetectorTask.java | 3 +- .../problemdetection/VipExpiryDetector.java | 26 +- .../nzbhydra/searching/CategoryProvider.java | 8 +- ...=> CustomQueryAndTitleMappingHandler.java} | 154 +- .../nzbhydra/searching/DuplicateDetector.java | 5 +- .../searching/IndexerForSearchSelector.java | 35 +- .../searching/IndexerInstantiator.java | 102 + .../searching/IndexerSearchCacheEntry.java | 2 + .../nzbhydra/searching/SearchCacheEntry.java | 9 +- .../searching/SearchModuleConfigProvider.java | 7 +- .../searching/SearchModuleProvider.java | 13 +- .../searching/SearchResultAcceptor.java | 21 +- .../org/nzbhydra/searching/SearchState.java | 2 + .../org/nzbhydra/searching/SearchWeb.java | 10 +- .../java/org/nzbhydra/searching/Searcher.java | 31 +- .../nzbhydra/searching/SortableMessage.java | 2 + .../searching/cleanup/HistoryCleanupTask.java | 2 +- .../cleanup/OldResultsCleanupTask.java | 2 +- .../searching/db/IdentifierKeyValuePair.java | 16 +- .../nzbhydra/searching/db/SearchEntity.java | 36 +- .../searching/db/SearchResultEntity.java | 88 +- .../DuplicateDetectionResult.java | 2 + .../FallbackSearchInitiatedEvent.java | 2 + .../IndexerSearchFinishedEvent.java | 2 + .../dtoseventsenums/IndexerSearchResult.java | 2 + .../IndexerSelectionEvent.java | 2 + .../OffsetAndLimitCalculation.java | 2 + .../dtoseventsenums/RejectionReason.java | 2 + .../dtoseventsenums/SearchMessageEvent.java | 2 + .../dtoseventsenums/SearchResultItem.java | 14 +- .../searchrequests/InternalData.java | 2 + .../searchrequests/SearchRequest.java | 37 +- .../searchrequests/SearchRequestFactory.java | 8 +- .../IndexerUniquenessScoreEntity.java | 21 +- .../nzbhydra/systemcontrol/SystemControl.java | 4 - .../systemcontrol/SystemControlWeb.java | 11 + .../nzbhydra/tasks/HydraTaskScheduler.java | 29 +- .../java/org/nzbhydra/update/UpdateData.java | 13 +- .../org/nzbhydra/update/UpdateManager.java | 54 +- .../java/org/nzbhydra/update/UpdatesWeb.java | 2 + .../main/java/org/nzbhydra/web/ApiError.java | 12 +- .../org/nzbhydra/web/BootstrappedDataTO.java | 2 + .../java/org/nzbhydra/web/ErrorHandler.java | 4 +- .../main/java/org/nzbhydra/web/HelpWeb.java | 2 +- .../nzbhydra/web/HydraErrorController.java | 47 + .../web/HydraRestControllerAdvice.java | 36 + .../java/org/nzbhydra/web/Interceptor.java | 8 +- .../main/java/org/nzbhydra/web/MainWeb.java | 9 +- .../java/org/nzbhydra/web/UrlCalculator.java | 11 +- .../org/nzbhydra/web/WebConfiguration.java | 5 +- .../org/nzbhydra/web/WebSocketConfig.java | 9 +- .../java/org/nzbhydra/web/WelcomeWeb.java | 5 +- .../AbstractAsyncClientHttpRequest.java | 88 - ...stractBufferingAsyncClientHttpRequest.java | 66 - .../HydraOkHttp3ClientHttpRequestFactory.java | 57 +- .../OkHttp3AsyncClientHttpRequest.java | 110 - .../webaccess/OkHttp3ClientHttpRequest.java | 5 - .../webaccess/OkHttp3ClientHttpResponse.java | 1 + .../main/java/org/nzbhydra/webaccess/Ssl.java | 4 +- .../org/nzbhydra/webaccess/WebAccess.java | 7 +- .../boot/SpringApplicationAotProcessor.java | 103 + .../META-INF/native-image/jni-config.json | 752 + .../predefined-classes-config.json | 8 + .../META-INF/native-image/proxy-config.json | 158 + .../META-INF/native-image/reflect-config.json | 36304 ++++++++++++++++ .../native-image/resource-config.json | 2728 ++ .../native-image/serialization-config.json | 12 + .../main/resources/META-INF/spring.factories | 4 +- core/src/main/resources/changelog.json | 135 +- core/src/main/resources/changelog.yaml | 3243 ++ .../config/application-build.properties | 4 + .../config/application-systemtest.properties | 1 + .../resources/config/application.properties | 16 +- core/src/main/resources/config/baseConfig.yml | 33 +- core/src/main/resources/config/logback.xml | 16 +- .../resources/migration/V1.0__INITIAL.sql | 181 - ...DEX_FOR_DOWNLOAD_STATUS_UPDATE_QUERIES.sql | 1 - .../V1.11__REMOVE_INDEXERSTATUSES.sql | 3 - .../migration/V1.12__ADD_MEDIAINFO_UNIQUE.sql | 4 - ....13__EXTEND_GENERICSTORAGE_DATA_LENGTH.sql | 2 - .../V1.14__SEARCHRESULT_LINK_LENGTH.sql | 2 - .../migration/V1.17__SHORTACCESS_AGAIN.sql | 28 - .../V1.18__SEARCH_HISTORY_INDEXES.sql | 4 - .../V1.19__ADD_TVINFO_IMDB_COLUMN.sql | 2 - .../migration/V1.1__NZBDOWNLOAD_USERAGENT.sql | 1 - .../V1.20__EMPTY_TV_AND_MOVIE_INFO.sql | 5 - ...NK_SEARCHENTITY_TO_INDEXERSEARCHENTITY.sql | 2 - .../V1.22__INDEXERUNIQUENESSSCORE.sql | 9 - ...1.23__INDEXERUNIQUENESSSCORE_DELETEOLD.sql | 2 - .../V1.24__REMOVE_OLD_SCORE_COLUMNS.sql | 4 - .../migration/V1.25__INDEXERSTATUS.sql | 12 - .../V1.2__DISTINGUISH_USERNAME_IP.sql | 5 - .../V1.3__DELETE_OLD_SEARCHRESULTS.sql | 2 - ....4__MAKE_SEARCHRESULT_INDEX_NON_UNIQUE.sql | 2 - .../V1.5__MAKE_SEARCH_QUERY_COLUMN_BIGGER.sql | 5 - .../V1.6__ADD_SEARCH_RESULT_TO_DOWNLOADS.sql | 17 - .../migration/V1.7__ADD_INDEXES_FOR_STATS.sql | 4 - ...NCREASE_QUERY_AND_ERROR_COLUMN_LENGTHs.sql | 2 - .../migration/V1.9__SET_DELETE_CASCADES.sql | 28 - .../main/resources/migration/V1__INITIAL.sql | 271 + .../resources/migration/V2__SEQUENCES.SQL | 54 + .../resources/migration/V4__NOTIFICATION.sql | 11 - .../V5__NOTIFICATION_ALLOW_NULL_URLS.sql | 2 - core/src/main/resources/static/js/nzbhydra.js | 85 +- .../main/resources/static/js/nzbhydra.js.map | 2 +- .../src/main/resources/static/js/templates.js | 144 +- core/src/main/resources/templates/error.html | 16 +- core/src/main/resources/wrapperHashes.json | 1 - core/src/main/resources/wrapperHashes2.json | 7 + .../test/java/org/nzbhydra/Experiments.java | 66 +- .../java/org/nzbhydra/NativeHintsTest.java | 66 + .../java/org/nzbhydra/NzbHandlerTest.java | 8 +- .../org/nzbhydra/api/CapsGeneratorTest.java | 8 +- .../nzbhydra/api/CategoryConverterTest.java | 19 +- .../org/nzbhydra/api/ExternalApiTest.java | 29 +- .../api/NewznabXmlTransformerTest.java | 16 +- .../org/nzbhydra/backup/BackupTaskTest.java | 45 +- .../org/nzbhydra/config/BaseConfigTest.java | 118 +- .../nzbhydra/config/CategoriesConfigTest.java | 28 +- .../config/DownloadingConfigTest.java | 30 +- .../config/IndexerCategoryConfigTest.java | 14 +- .../nzbhydra/config/IndexerConfigTest.java | 14 +- .../config/SearchSourceRestrictionTest.java | 77 +- .../ConfigMigrationStep005to006Test.java | 4 +- .../config/migration/ConfigMigrationTest.java | 34 +- .../org/nzbhydra/database/ThreadedTest.java | 23 +- .../downloading/DownloadResultTest.java | 16 +- .../DownloadStatusUpdaterTest.java | 23 +- .../downloading/FileDownloadEntityTest.java | 69 + .../IndexerUniquenessScoreSaverTest.java | 51 +- .../downloading/NzbDownloadEntityTest.java | 6 +- .../downloading/NzbDownloadStatusTest.java | 50 +- .../org/nzbhydra/downloading/NzbGetTest.java | 38 - .../downloaders/DownloaderStatusTest.java | 4 +- .../downloaders/DownloaderTest.java | 22 +- .../sabnzbd/HistoryResponseTest.java | 4 +- .../sabnzbd/QueueResponseTest.java | 4 +- .../torrents/TorrentFileHandlerTest.java | 34 +- .../externaltools/ExternalToolsTest.java | 24 +- .../fortests/NewznabResponseBuilderTest.java | 26 +- .../historystats/StatsComponentTest.java | 627 +- .../java/org/nzbhydra/indexers/AnizbTest.java | 38 +- .../org/nzbhydra/indexers/BinsearchTest.java | 111 +- .../org/nzbhydra/indexers/DevIndexerTest.java | 10 +- .../IndexerStatusesCleanupTaskTest.java | 15 +- .../org/nzbhydra/indexers/IndexerTest.java | 107 +- .../indexers/IndexerWebAccessTest.java | 14 +- .../org/nzbhydra/indexers/NewznabTest.java | 393 +- .../org/nzbhydra/indexers/NzbGeekTest.java | 13 +- .../org/nzbhydra/indexers/NzbIndexTest.java | 59 +- .../indexers/OkHttpHandshakeAlert.java | 14 +- .../java/org/nzbhydra/indexers/SslTest.java | 20 +- .../capscheck/NewznabCheckerTest.java | 85 +- .../indexers/torznab/TorznabTest.java | 79 +- .../nzbhydra/logging/LogAnonymizerTest.java | 40 +- ...eDataRemovingPatternLayoutEncoderTest.java | 4 +- ...ttCapsCustomQueryAndTitleMappingTest.java} | 18 +- ...ssCapsCustomQueryAndTitleMappingTest.java} | 35 +- .../RssCustomQueryAndTitleMappingTest.java | 235 + .../org/nzbhydra/mapping/RssMappingTest.java | 234 - ...orznabCustomQueryAndTitleMappingTest.java} | 49 +- ...> JsonCustomQueryAndTitleMappingTest.java} | 19 +- .../nzbhydra/mediainfo/CustomTmdbTest.java | 40 - .../nzbhydra/mediainfo/InfoProviderTest.java | 58 +- .../nzbhydra/mediainfo/TmdbHandlerTest.java | 45 - .../org/nzbhydra/news/NewsProviderTest.java | 41 +- .../notifications/NotificationEntityTest.java | 46 + .../OutOfMemoryDetectorTest.java | 8 +- .../searching/CategoryProviderTest.java | 401 +- ...ustomQueryAndTitleMappingHandlerTest.java} | 52 +- .../searching/DuplicateDetectorTest.java | 19 +- .../IndexerForSearchSelectorTest.java | 943 +- .../InternalSearchResultProcessorTest.java | 4 +- .../nzbhydra/searching/SearchEntityTest.java | 45 +- .../searching/SearchResultAcceptorTest.java | 98 +- .../searching/SearchResultItemTest.java | 44 +- .../nzbhydra/searching/SearcherUnitTest.java | 65 +- .../searchrequests/SearchRequestTest.java | 22 +- .../nzbhydra/update/UpdateManagerTest.java | 43 +- .../org/nzbhydra/web/UrlCalculatorTest.java | 39 +- ...raOkHttp3ClientHttpRequestFactoryTest.java | 97 +- .../resources/config/application.properties | 8 +- core/ui-src/html/custom-mapping-help.html | 6 +- core/ui-src/html/news-modal.html | 2 +- .../html/states/notification-history.html | 8 +- core/ui-src/html/states/search.html | 170 +- .../js/directives/downloaderStatusFooter.js | 2 +- .../js/directives/hydra-checks-footer.js | 26 +- core/ui-src/js/generic-error-handler.js | 11 +- core/ui-src/js/search-controller.js | 4 +- core/ui-src/js/search-history-controller.js | 10 + docker/beta/.gitignore | 4 + docker/beta/dockerfile | 17 + docker/docker-compose.yaml | 68 + docker/nativeTest/.gitignore | 4 + docker/nativeTest/buildHydraDocker.sh | 2 + docker/nativeTest/dockerfile | 14 + docker/radarr/.gitignore | 2 + docker/radarr/data/config.xml | 16 + docker/radarr/docker-compose.yml | 14 + docker/sonarr/.gitignore | 2 + docker/sonarr/data/config.xml | 16 + docker/sonarr/docker-compose.yml | 15 + docker/waitForHealthyContainer.sh | 1 + misc/build-and-release.cmd | 161 - misc/build-and-release.ps1 | 273 + misc/copypoms.cmd | 7 - misc/docker/.dockerignore | 1 - misc/docker/.gitignore | 7 - misc/docker/Dockerfile | 17 - misc/docker/builddocker.cmd | 21 - misc/docker/builddocker.sh | 27 - misc/docker/docker-compose.yml | 7 - misc/docker/notes | 12 - misc/init.cmd | 32 - nativeNotes.md | 127 + other/discordbot/pom.xml | 120 - .../java/org/nzbhydra/discordbot/Asset.java | 114 - .../org/nzbhydra/discordbot/DiscordBot.java | 190 - .../java/org/nzbhydra/discordbot/Release.java | 165 - other/github-release-plugin/.gitignore | 3 +- other/github-release-plugin/pom.xml | 55 +- .../ChangelogGeneratorMojo.java | 24 +- .../PublishOnDiscordMojo.java | 87 + .../mavenreleaseplugin/ReleaseMojo.java | 246 +- .../SetReleaseFinalMojo.java | 25 +- .../ChangelogGeneratorMojoTest.java | 5 +- .../mavenreleaseplugin/PrecheckMojoTest.java | 45 +- .../PublishOnDiscordMojoTest.java | 28 + .../mavenreleaseplugin/ReleaseMojoTest.java | 55 +- .../SetReleaseFinalMojoTest.java | 14 +- .../mavenreleaseplugin/changelog-001.json | 15 +- .../mavenreleaseplugin/changelog.json.orig | 16 - .../github/mavenreleaseplugin/changelog.yaml | 12 + .../mavenreleaseplugin/changelog.yaml.orig | 12 + .../changelogGeneratorPom.xml | 2 +- .../mavenreleaseplugin/changelogOld.json | 11 - .../mavenreleaseplugin/changelogOld.yaml | 5 + .../discordPublisherPom.xml | 25 + .../mavenreleaseplugin/genericAsset.txt | 1 + .../github/mavenreleaseplugin/pomPrecheck.xml | 28 + .../pomWithChangelogWrongLatestEntry.xml | 9 +- .../mavenreleaseplugin/pomWithToken.xml | 4 +- .../mavenreleaseplugin/pomWithTokenFile.xml | 9 +- .../mavenreleaseplugin/pomWithoutToken.xml | 4 +- .../setReleaseToFinalPom.xml | 3 +- other/loadtest/pom.xml | 32 + .../org/nzbhydra/loadtest/GatlingRunner.java | 18 + .../nzbhydra/loadtest/SearchSimulation.java | 54 + other/mockserver/pom.xml | 101 +- .../org/nzbhydra/mockserver/MockGithub.java | 27 +- .../org/nzbhydra/mockserver/MockNewznab.java | 21 +- .../org/nzbhydra/mockserver/MockSabnzb.java | 18 + .../nzbhydra/mockserver/WebConfiguration.java | 21 +- .../application-systemtest.properties | 1 + .../nzbhydra/MockserverApplicationTests.java | 6 +- other/pom.xml | 23 +- other/sockslib/pom.xml | 15 +- .../sockslib/common/SSLConfiguration.java | 9 +- .../sockslib/quickstart/Socks5Server.java | 3 +- .../sockslib/quickstart/TCPTimeClient.java | 3 +- .../sockslib/quickstart/TCPTimeServer.java | 3 +- .../java/sockslib/server/io/StreamPipe.java | 6 +- .../main/java/sockslib/utils/Arguments.java | 3 +- other/wrapper/.gitignore | 4 +- other/wrapper/nzbhydra2wrapper.py | 136 +- other/wrapper/nzbhydra2wrapperPy3.py | 238 +- other/wrapper/pyInstaller/buildWrapper.sh | 11 +- .../pyInstaller/{ => linux}/NZBHydra2.spec | 0 .../pyInstaller/windows/NzbHydra2.spec | 27 + .../windows/nzbhydra2wrapperWindows.py | 26 + .../wrapper/pyInstaller/windows/systemTray.py | 55 + .../NZBHydra2 Console.spec | 0 pom.xml | 63 +- readme.md | 34 +- releases/.gitignore | 1 + releases/generic-release/.gitignore | 4 + releases/generic-release/bin.xml | 38 + releases/generic-release/pom.xml | 47 + releases/linux-release/.gitignore | 4 - releases/linux-release/bin.xml | 8 - releases/linux-release/include/nzbhydra2 | 4 +- releases/linux-release/pom.xml | 4 +- releases/pom.xml | 21 +- releases/windows-release/.gitignore | 4 +- releases/windows-release/bin.xml | 8 - .../include/NZBHydra2 Console.exe | 4 +- .../windows-release/include/NZBHydra2.exe | 4 +- releases/windows-release/pom.xml | 4 +- shared/assertions/pom.xml | 49 + .../AbstractGenericResponseAssert.java | 83 + .../java/org/nzbhydra/AssertFactories.java | 27 + .../main/java/org/nzbhydra/Assertions.java | 1622 + .../org/nzbhydra/GenericResponseAssert.java | 32 + .../java/org/nzbhydra/SoftAssertions.java | 1615 + .../backup/AbstractBackupEntryAssert.java | 69 + .../nzbhydra/backup/BackupEntryAssert.java | 32 + .../config/AbstractBaseConfigAssert.java | 354 + .../AbstractHistoryUserInfoTypeAssert.java | 20 + .../config/AbstractLoggingConfigAssert.java | 434 + .../config/AbstractMainConfigAssert.java | 1760 + .../AbstractNotificationConfigAssert.java | 484 + ...AbstractNotificationConfigEntryAssert.java | 141 + .../config/AbstractProxyTypeAssert.java | 20 + .../config/AbstractRestartRequiredAssert.java | 20 + .../config/AbstractSearchSourceAssert.java | 20 + ...AbstractSearchSourceRestrictionAssert.java | 58 + .../config/AbstractSearchingConfigAssert.java | 2567 ++ .../org/nzbhydra/config/BaseConfigAssert.java | 32 + .../config/HistoryUserInfoTypeAssert.java | 32 + .../nzbhydra/config/LoggingConfigAssert.java | 32 + .../org/nzbhydra/config/MainConfigAssert.java | 32 + .../config/NotificationConfigAssert.java | 32 + .../config/NotificationConfigEntryAssert.java | 32 + .../org/nzbhydra/config/ProxyTypeAssert.java | 32 + .../config/RestartRequiredAssert.java | 32 + .../nzbhydra/config/SearchSourceAssert.java | 32 + .../config/SearchSourceRestrictionAssert.java | 32 + .../config/SearchingConfigAssert.java | 32 + .../config/auth/AbstractAuthConfigAssert.java | 726 + .../config/auth/AbstractAuthTypeAssert.java | 20 + .../auth/AbstractUserAuthConfigAssert.java | 221 + .../config/auth/AuthConfigAssert.java | 32 + .../nzbhydra/config/auth/AuthTypeAssert.java | 32 + .../config/auth/UserAuthConfigAssert.java | 32 + .../AbstractCategoriesConfigAssert.java | 248 + .../category/AbstractCategoryAssert.java | 868 + ...ctNewznabCategoriesDeserializerAssert.java | 20 + ...ractNewznabCategoriesSerializerAssert.java | 20 + .../category/CategoriesConfigAssert.java | 32 + .../config/category/CategoryAssert.java | 32 + .../NewznabCategoriesDeserializerAssert.java | 32 + .../NewznabCategoriesSerializerAssert.java | 32 + .../AbstractDownloadTypeAssert.java | 20 + .../AbstractDownloaderConfigAssert.java | 337 + .../AbstractDownloadingConfigAssert.java | 444 + .../AbstractFileDownloadAccessTypeAssert.java | 20 + .../AbstractNzbAddingTypeAssert.java | 20 + .../downloading/DownloadTypeAssert.java | 32 + .../downloading/DownloaderConfigAssert.java | 32 + .../downloading/DownloadingConfigAssert.java | 32 + .../FileDownloadAccessTypeAssert.java | 32 + .../downloading/NzbAddingTypeAssert.java | 32 + .../indexer/AbstractBackendTypeAssert.java | 20 + .../AbstractCapsCheckRequestAssert.java | 69 + .../AbstractCheckCapsResponseAssert.java | 121 + .../AbstractIndexerCategoryConfigAssert.java | 306 + .../indexer/AbstractIndexerConfigAssert.java | 1656 + .../indexer/AbstractQueryFormatAssert.java | 20 + .../AbstractSearchModuleTypeAssert.java | 20 + .../config/indexer/BackendTypeAssert.java | 32 + .../indexer/CapsCheckRequestAssert.java | 32 + .../indexer/CheckCapsResponseAssert.java | 32 + .../indexer/IndexerCategoryConfigAssert.java | 32 + .../config/indexer/IndexerConfigAssert.java | 32 + .../config/indexer/QueryFormatAssert.java | 32 + .../indexer/SearchModuleTypeAssert.java | 32 + .../mediainfo/AbstractMediaIdTypeAssert.java | 20 + .../config/mediainfo/MediaIdTypeAssert.java | 32 + .../AbstractNotificationEventTypeAssert.java | 20 + .../NotificationEventTypeAssert.java | 32 + .../AbstractAffectedValueAssert.java | 20 + ...tractCustomQueryAndTitleMappingAssert.java | 141 + .../searching/AbstractSearchTypeAssert.java | 20 + .../config/searching/AffectedValueAssert.java | 32 + .../CustomQueryAndTitleMappingAssert.java | 32 + .../config/searching/SearchTypeAssert.java | 32 + .../AbstractSensitiveDataAssert.java | 20 + .../config/sensitive/SensitiveDataAssert.java | 32 + .../AbstractAddFilesRequestAssert.java | 234 + .../AbstractDownloaderTypeAssert.java | 20 + .../AbstractFileDownloadEntityTOAssert.java | 309 + .../AbstractFileDownloadStatusAssert.java | 58 + .../AbstractFileZipResponseAssert.java | 436 + .../downloading/AddFilesRequestAssert.java | 32 + .../downloading/DownloaderTypeAssert.java | 32 + .../FileDownloadEntityTOAssert.java | 32 + .../downloading/FileDownloadStatusAssert.java | 32 + .../downloading/FileZipResponseAssert.java | 32 + .../AbstractAddNzbsResponseAssert.java | 412 + .../downloaders/AbstractConvertersAssert.java | 20 + .../AbstractDownloaderStatusAssert.java | 608 + .../downloaders/AddNzbsResponseAssert.java | 32 + .../downloaders/ConvertersAssert.java | 32 + .../downloaders/DownloaderStatusAssert.java | 32 + .../mapping/AbstractAddNzbResponseAssert.java | 248 + .../AbstractCategoriesResponseAssert.java | 185 + .../mapping/AbstractHistoryAssert.java | 330 + .../mapping/AbstractHistoryEntryAssert.java | 954 + .../AbstractHistoryResponseAssert.java | 45 + .../sabnzbd/mapping/AbstractQueueAssert.java | 1426 + .../mapping/AbstractQueueEntryAssert.java | 477 + .../mapping/AbstractQueueResponseAssert.java | 45 + .../mapping/AbstractStageLogEntryAssert.java | 210 + .../sabnzbd/mapping/AddNzbResponseAssert.java | 32 + .../mapping/CategoriesResponseAssert.java | 32 + .../sabnzbd/mapping/HistoryAssert.java | 32 + .../sabnzbd/mapping/HistoryEntryAssert.java | 32 + .../mapping/HistoryResponseAssert.java | 32 + .../sabnzbd/mapping/QueueAssert.java | 32 + .../sabnzbd/mapping/QueueEntryAssert.java | 32 + .../sabnzbd/mapping/QueueResponseAssert.java | 32 + .../sabnzbd/mapping/StageLogEntryAssert.java | 32 + .../AbstractAddRequestAssert.java | 785 + .../externaltools/AddRequestAssert.java | 32 + .../AbstractFilterDefinitionAssert.java | 107 + .../AbstractFilterModelAssert.java | 20 + .../historystats/AbstractSortModelAssert.java | 69 + .../AbstractStatsResponseAssert.java | 2602 ++ .../historystats/FilterDefinitionAssert.java | 32 + .../historystats/FilterModelAssert.java | 32 + .../historystats/SortModelAssert.java | 32 + .../historystats/StatsResponseAssert.java | 32 + .../AbstractAverageResponseTimeAssert.java | 148 + .../AbstractCountPerDayOfWeekAssert.java | 69 + .../AbstractCountPerHourOfDayAssert.java | 69 + ...ownloadOrSearchSharePerUserOrIpAssert.java | 121 + .../stats/AbstractDownloadPerAgeAssert.java | 69 + .../AbstractDownloadPerAgeStatsAssert.java | 282 + .../stats/AbstractHistoryRequestAssert.java | 193 + ...tractIndexerApiAccessStatsEntryAssert.java | 199 + .../AbstractIndexerDownloadShareAssert.java | 121 + .../stats/AbstractIndexerScoreAssert.java | 117 + .../stats/AbstractStatsRequestAssert.java | 715 + ...ctSuccessfulDownloadsPerIndexerAssert.java | 169 + .../stats/AbstractUserAgentShareAssert.java | 121 + .../stats/AverageResponseTimeAssert.java | 32 + .../stats/CountPerDayOfWeekAssert.java | 32 + .../stats/CountPerHourOfDayAssert.java | 32 + ...ownloadOrSearchSharePerUserOrIpAssert.java | 32 + .../stats/DownloadPerAgeAssert.java | 32 + .../stats/DownloadPerAgeStatsAssert.java | 32 + .../stats/HistoryRequestAssert.java | 32 + .../IndexerApiAccessStatsEntryAssert.java | 32 + .../stats/IndexerDownloadShareAssert.java | 32 + .../stats/IndexerScoreAssert.java | 32 + .../stats/StatsRequestAssert.java | 32 + .../SuccessfulDownloadsPerIndexerAssert.java | 32 + .../stats/UserAgentShareAssert.java | 32 + .../AbstractIndexerEntityTOAssert.java | 69 + .../indexers/IndexerEntityTOAssert.java | 32 + .../AbstractSemanticVersionAssert.java | 141 + .../mapping/SemanticVersionAssert.java | 32 + .../AbstractChangelogChangeEntryAssert.java | 69 + .../AbstractChangelogVersionEntryAssert.java | 296 + .../changelog/ChangelogChangeEntryAssert.java | 32 + .../ChangelogVersionEntryAssert.java | 32 + .../mapping/github/AbstractAssetAssert.java | 285 + .../mapping/github/AbstractReleaseAssert.java | 546 + .../nzbhydra/mapping/github/AssetAssert.java | 32 + .../mapping/github/ReleaseAssert.java | 32 + .../AbstractActionAttributeAssert.java | 58 + .../AbstractNewznabParametersAssert.java | 1166 + .../AbstractNewznabResponseAssert.java | 69 + .../newznab/AbstractOutputTypeAssert.java | 20 + .../newznab/ActionAttributeAssert.java | 32 + .../newznab/NewznabParametersAssert.java | 32 + .../newznab/NewznabResponseAssert.java | 32 + .../mapping/newznab/OutputTypeAssert.java | 32 + .../builder/AbstractRssBuilderAssert.java | 20 + .../AbstractRssChannelBuilderAssert.java | 20 + .../builder/AbstractRssItemBuilderAssert.java | 20 + .../newznab/builder/RssBuilderAssert.java | 32 + .../builder/RssChannelBuilderAssert.java | 32 + .../newznab/builder/RssItemBuilderAssert.java | 32 + ...AbstractJsonPubdateDeserializerAssert.java | 20 + .../AbstractJsonPubdateSerializerAssert.java | 20 + .../AbstractNewznabJsonChannelAssert.java | 402 + ...tractNewznabJsonChannelResponseAssert.java | 45 + .../AbstractNewznabJsonEnclosureAssert.java | 45 + ...tNewznabJsonEnclosureAttributesAssert.java | 93 + .../json/AbstractNewznabJsonErrorAssert.java | 69 + .../json/AbstractNewznabJsonImageAssert.java | 117 + .../json/AbstractNewznabJsonItemAssert.java | 402 + .../AbstractNewznabJsonItemAttrAssert.java | 45 + ...stractNewznabJsonItemAttributesAssert.java | 69 + ...ctNewznabJsonResponseAttributesAssert.java | 69 + .../json/AbstractNewznabJsonRootAssert.java | 117 + ...stractNewznabJsonRootAttributesAssert.java | 45 + .../json/JsonPubdateDeserializerAssert.java | 32 + .../json/JsonPubdateSerializerAssert.java | 32 + .../json/NewznabJsonChannelAssert.java | 32 + .../NewznabJsonChannelResponseAssert.java | 32 + .../json/NewznabJsonEnclosureAssert.java | 32 + .../NewznabJsonEnclosureAttributesAssert.java | 32 + .../newznab/json/NewznabJsonErrorAssert.java | 32 + .../newznab/json/NewznabJsonImageAssert.java | 32 + .../newznab/json/NewznabJsonItemAssert.java | 32 + .../json/NewznabJsonItemAttrAssert.java | 32 + .../json/NewznabJsonItemAttributesAssert.java | 32 + .../NewznabJsonResponseAttributesAssert.java | 32 + .../newznab/json/NewznabJsonRootAssert.java | 32 + .../json/NewznabJsonRootAttributesAssert.java | 32 + ...bstractCapsJsonCategoriesHolderAssert.java | 185 + .../caps/AbstractCapsJsonCategoryAssert.java | 210 + ...tractCapsJsonCategoryAttributesAssert.java | 69 + .../AbstractCapsJsonIdAttributesAssert.java | 69 + .../caps/AbstractCapsJsonLimitsAssert.java | 45 + ...bstractCapsJsonLimitsAttributesAssert.java | 45 + .../AbstractCapsJsonRegistrationAssert.java | 45 + ...tCapsJsonRegistrationAttributesAssert.java | 69 + .../json/caps/AbstractCapsJsonRootAssert.java | 141 + ...apsJsonSearchIdAttributesHolderAssert.java | 45 + .../caps/AbstractCapsJsonSearchingAssert.java | 141 + .../caps/AbstractCapsJsonServerAssert.java | 45 + ...bstractCapsJsonServerAttributesAssert.java | 189 + .../caps/CapsJsonCategoriesHolderAssert.java | 32 + .../json/caps/CapsJsonCategoryAssert.java | 32 + .../CapsJsonCategoryAttributesAssert.java | 32 + .../json/caps/CapsJsonIdAttributesAssert.java | 32 + .../json/caps/CapsJsonLimitsAssert.java | 32 + .../caps/CapsJsonLimitsAttributesAssert.java | 32 + .../json/caps/CapsJsonRegistrationAssert.java | 32 + .../CapsJsonRegistrationAttributesAssert.java | 32 + .../newznab/json/caps/CapsJsonRootAssert.java | 32 + ...apsJsonSearchIdAttributesHolderAssert.java | 32 + .../json/caps/CapsJsonSearchingAssert.java | 32 + .../json/caps/CapsJsonServerAssert.java | 32 + .../caps/CapsJsonServerAttributesAssert.java | 32 + .../AbstractNewznabMockBuilderAssert.java | 45 + .../AbstractNewznabMockRequestAssert.java | 444 + .../mock/NewznabMockBuilderAssert.java | 32 + .../mock/NewznabMockRequestAssert.java | 32 + .../xml/AbstractJaxbPubdateAdapterAssert.java | 20 + .../xml/AbstractNewznabAttributeAssert.java | 69 + .../AbstractNewznabXmlApilimitsAssert.java | 165 + .../xml/AbstractNewznabXmlChannelAssert.java | 378 + .../AbstractNewznabXmlEnclosureAssert.java | 93 + .../xml/AbstractNewznabXmlErrorAssert.java | 68 + .../xml/AbstractNewznabXmlGuidAssert.java | 83 + .../xml/AbstractNewznabXmlItemAssert.java | 754 + .../xml/AbstractNewznabXmlResponseAssert.java | 69 + .../xml/AbstractNewznabXmlRootAssert.java | 92 + .../newznab/xml/AbstractXmlAssert.java | 69 + .../newznab/xml/JaxbPubdateAdapterAssert.java | 32 + .../newznab/xml/NewznabAttributeAssert.java | 32 + .../xml/NewznabXmlApilimitsAssert.java | 32 + .../newznab/xml/NewznabXmlChannelAssert.java | 32 + .../xml/NewznabXmlEnclosureAssert.java | 32 + .../newznab/xml/NewznabXmlErrorAssert.java | 32 + .../newznab/xml/NewznabXmlGuidAssert.java | 32 + .../newznab/xml/NewznabXmlItemAssert.java | 32 + .../newznab/xml/NewznabXmlResponseAssert.java | 32 + .../newznab/xml/NewznabXmlRootAssert.java | 32 + .../mapping/newznab/xml/XmlAssert.java | 32 + .../caps/AbstractCapsXmlCategoriesAssert.java | 185 + .../caps/AbstractCapsXmlCategoryAssert.java | 234 + .../xml/caps/AbstractCapsXmlLimitsAssert.java | 69 + .../caps/AbstractCapsXmlRetentionAssert.java | 45 + .../xml/caps/AbstractCapsXmlRootAssert.java | 141 + .../xml/caps/AbstractCapsXmlSearchAssert.java | 107 + .../caps/AbstractCapsXmlSearchingAssert.java | 141 + .../xml/caps/AbstractCapsXmlServerAssert.java | 165 + .../xml/caps/CapsXmlCategoriesAssert.java | 32 + .../xml/caps/CapsXmlCategoryAssert.java | 32 + .../newznab/xml/caps/CapsXmlLimitsAssert.java | 32 + .../xml/caps/CapsXmlRetentionAssert.java | 32 + .../newznab/xml/caps/CapsXmlRootAssert.java | 32 + .../newznab/xml/caps/CapsXmlSearchAssert.java | 32 + .../xml/caps/CapsXmlSearchingAssert.java | 32 + .../newznab/xml/caps/CapsXmlServerAssert.java | 32 + .../AbstractJacketCapsXmlIndexerAssert.java | 93 + .../AbstractJacketCapsXmlRootAssert.java | 185 + .../jackett/JacketCapsXmlIndexerAssert.java | 32 + .../caps/jackett/JacketCapsXmlRootAssert.java | 32 + .../mediainfo/AbstractMediaInfoTOAssert.java | 213 + .../nzbhydra/mediainfo/MediaInfoTOAssert.java | 32 + .../AbstractIndexerSearchEntityTOAssert.java | 141 + .../AbstractSearchResponseAssert.java | 566 + .../IndexerSearchEntityTOAssert.java | 32 + .../searching/SearchResponseAssert.java | 32 + ...bstractIdentifierKeyValuePairTOAssert.java | 69 + .../db/AbstractSearchEntityTOAssert.java | 498 + .../AbstractSearchResultEntityTOAssert.java | 261 + .../db/IdentifierKeyValuePairTOAssert.java | 32 + .../searching/db/SearchEntityTOAssert.java | 32 + .../db/SearchResultEntityTOAssert.java | 32 + .../AbstractIndexerSearchMetaDataAssert.java | 341 + ...AbstractSearchRequestParametersAssert.java | 656 + .../AbstractSearchResultWebTOAssert.java | 741 + .../IndexerSearchMetaDataAssert.java | 32 + .../SearchRequestParametersAssert.java | 32 + .../SearchResultWebTOAssert.java | 32 + .../AbstractReflectionMarkerAssert.java | 20 + .../springnative/ReflectionMarkerAssert.java | 32 + shared/mapping/pom.xml | 70 +- .../java/org/nzbhydra/GenericResponse.java | 2 + .../java/org/nzbhydra/backup/BackupEntry.java | 33 + .../java/org/nzbhydra/config/BaseConfig.java | 63 + .../nzbhydra/config/HistoryUserInfoType.java | 0 .../org/nzbhydra/config/LoggingConfig.java | 45 + .../java/org/nzbhydra/config/MainConfig.java | 153 + .../nzbhydra/config/NotificationConfig.java | 38 + .../config/NotificationConfigEntry.java | 51 + .../java/org/nzbhydra/config}/ProxyType.java | 4 +- .../org/nzbhydra/config/RestartRequired.java | 0 .../org/nzbhydra/config/SearchSource.java | 29 + .../config/SearchSourceRestriction.java | 15 + .../org/nzbhydra/config/SearchingConfig.java | 114 + .../org/nzbhydra/config/auth/AuthConfig.java | 63 + .../org/nzbhydra/config/auth/AuthType.java | 0 .../nzbhydra/config/auth/UserAuthConfig.java | 33 +- .../config/category/CategoriesConfig.java | 69 + .../nzbhydra/config/category/Category.java | 51 +- .../NewznabCategoriesDeserializer.java | 0 .../category/NewznabCategoriesSerializer.java | 0 .../config/downloading/DownloadType.java | 0 .../config/downloading/DownloaderConfig.java | 63 + .../config/downloading/DownloadingConfig.java | 65 + .../downloading/FileDownloadAccessType.java | 0 .../config/downloading/NzbAddingType.java | 0 .../nzbhydra/config/indexer/BackendType.java | 23 + .../config/indexer}/CapsCheckRequest.java | 11 +- .../config/indexer}/CheckCapsResponse.java | 7 +- .../config/indexer/IndexerCategoryConfig.java | 3 + .../config/indexer/IndexerConfig.java | 104 +- .../nzbhydra/config/indexer/QueryFormat.java | 23 + .../config/indexer/SearchModuleType.java | 0 .../config}/mediainfo/MediaIdType.java | 4 +- .../notification}/NotificationEventType.java | 4 +- .../config/searching/AffectedValue.java | 23 + .../searching/CustomQueryAndTitleMapping.java | 67 + .../config/searching}/SearchType.java | 6 +- .../config/sensitive/SensitiveData.java | 0 .../nzbhydra/downloading/AddFilesRequest.java | 63 +- .../nzbhydra}/downloading/DownloaderType.java | 4 +- .../downloading/FileDownloadEntityTO.java | 32 + .../downloading/FileDownloadStatus.java | 15 +- .../nzbhydra/downloading/FileZipResponse.java | 2 + .../downloaders/AddNzbsResponse.java | 2 + .../downloading/downloaders/Converters.java | 0 .../downloaders/DownloaderStatus.java | 7 +- .../sabnzbd/mapping/AddNzbResponse.java | 2 + .../sabnzbd/mapping/CategoriesResponse.java | 2 + .../downloaders/sabnzbd/mapping/History.java | 2 + .../sabnzbd/mapping/HistoryEntry.java | 2 + .../sabnzbd/mapping/HistoryResponse.java | 2 + .../downloaders/sabnzbd/mapping/Queue.java | 2 + .../sabnzbd/mapping/QueueEntry.java | 2 + .../sabnzbd/mapping/QueueResponse.java | 2 + .../sabnzbd/mapping/StageLogEntry.java | 2 + .../nzbhydra/externaltools/AddRequest.java | 8 +- .../historystats/FilterDefinition.java | 2 + .../nzbhydra/historystats/FilterModel.java | 0 .../org/nzbhydra/historystats/SortModel.java | 2 + .../nzbhydra/historystats/StatsResponse.java | 69 + .../stats/AverageResponseTime.java | 2 + .../historystats/stats/CountPerDayOfWeek.java | 42 + .../historystats/stats/CountPerHourOfDay.java | 2 + .../DownloadOrSearchSharePerUserOrIp.java | 2 + .../historystats/stats/DownloadPerAge.java | 2 + .../stats/DownloadPerAgeStats.java | 2 + .../historystats/stats/HistoryRequest.java | 2 + .../stats/IndexerApiAccessStatsEntry.java | 2 + .../stats/IndexerDownloadShare.java | 2 + .../historystats/stats/IndexerScore.java | 2 + .../historystats/stats/StatsRequest.java | 2 + .../stats/SuccessfulDownloadsPerIndexer.java | 2 + .../historystats/stats/UserAgentShare.java | 2 + .../nzbhydra/indexers/IndexerEntityTO.java | 23 + .../org/nzbhydra/mapping/SemanticVersion.java | 3 + .../changelog/ChangelogChangeEntry.java | 2 + .../changelog/ChangelogVersionEntry.java | 2 + .../org/nzbhydra/mapping/github/Asset.java | 2 + .../org/nzbhydra/mapping/github/Release.java | 2 + .../mapping/newznab/NewznabParameters.java | 10 +- .../mapping/newznab/NewznabResponse.java | 4 +- .../newznab/json/NewznabJsonChannel.java | 2 + .../json/NewznabJsonChannelResponse.java | 2 + .../newznab/json/NewznabJsonEnclosure.java | 2 + .../json/NewznabJsonEnclosureAttributes.java | 2 + .../newznab/json/NewznabJsonError.java | 2 + .../newznab/json/NewznabJsonImage.java | 2 + .../mapping/newznab/json/NewznabJsonItem.java | 2 + .../newznab/json/NewznabJsonItemAttr.java | 2 + .../json/NewznabJsonItemAttributes.java | 2 + .../json/NewznabJsonResponseAttributes.java | 2 + .../mapping/newznab/json/NewznabJsonRoot.java | 2 + .../json/NewznabJsonRootAttributes.java | 2 + .../json/caps/CapsJsonCategoriesHolder.java | 2 + .../newznab/json/caps/CapsJsonCategory.java | 2 + .../json/caps/CapsJsonCategoryAttributes.java | 2 + .../json/caps/CapsJsonIdAttributes.java | 2 + .../newznab/json/caps/CapsJsonLimits.java | 2 + .../json/caps/CapsJsonLimitsAttributes.java | 2 + .../json/caps/CapsJsonRegistration.java | 2 + .../caps/CapsJsonRegistrationAttributes.java | 2 + .../newznab/json/caps/CapsJsonRoot.java | 2 + .../CapsJsonSearchIdAttributesHolder.java | 2 + .../newznab/json/caps/CapsJsonSearching.java | 2 + .../newznab/json/caps/CapsJsonServer.java | 2 + .../json/caps/CapsJsonServerAttributes.java | 2 + .../newznab/mock/NewznabMockBuilder.java | 13 +- .../newznab/mock/NewznabMockRequest.java | 2 + .../newznab/xml/JaxbPubdateAdapter.java | 4 +- .../mapping/newznab/xml/NewznabAttribute.java | 9 +- .../newznab/xml/NewznabXmlApilimits.java | 10 +- .../newznab/xml/NewznabXmlChannel.java | 8 +- .../newznab/xml/NewznabXmlEnclosure.java | 10 +- .../mapping/newznab/xml/NewznabXmlError.java | 10 +- .../mapping/newznab/xml/NewznabXmlGuid.java | 12 +- .../mapping/newznab/xml/NewznabXmlItem.java | 14 +- .../newznab/xml/NewznabXmlResponse.java | 10 +- .../mapping/newznab/xml/NewznabXmlRoot.java | 12 +- .../org/nzbhydra/mapping/newznab/xml/Xml.java | 15 +- .../newznab/xml/caps/CapsXmlCategories.java | 10 +- .../newznab/xml/caps/CapsXmlCategory.java | 12 +- .../newznab/xml/caps/CapsXmlLimits.java | 10 +- .../newznab/xml/caps/CapsXmlRetention.java | 8 +- .../mapping/newznab/xml/caps/CapsXmlRoot.java | 13 +- .../newznab/xml/caps/CapsXmlSearch.java | 8 +- .../newznab/xml/caps/CapsXmlSearching.java | 10 +- .../newznab/xml/caps/CapsXmlServer.java | 8 +- .../caps/jackett/JacketCapsXmlIndexer.java | 12 +- .../xml/caps/jackett/JacketCapsXmlRoot.java | 10 +- .../mapping/newznab/xml/package-info.java | 7 +- .../org/nzbhydra/mediainfo/MediaInfoTO.java | 34 + .../org/nzbhydra/news/NewsEntryForWeb.java | 33 + .../notifications/NotificationEntityTO.java | 41 + .../NotificationMessageType.java | 24 + .../searching/IndexerSearchEntityTO.java | 26 + .../nzbhydra/searching/SearchResponse.java | 2 + .../db/IdentifierKeyValuePairTO.java | 32 + .../nzbhydra/searching/db/SearchEntityTO.java | 51 + .../searching/db/SearchResultEntityTO.java | 48 + .../IndexerSearchMetaData.java | 2 + .../SearchRequestParameters.java | 2 + .../dtoseventsenums/SearchResultWebTO.java | 5 +- .../springnative/ReflectionMarker.java | 27 + .../nzbhydra/mapping/SemanticVersionTest.java | 59 +- .../newznab/NewznabParametersTest.java | 9 +- .../newznab/builder/RssItemBuilderTest.java | 40 +- shared/pom.xml | 5 +- tests/.gitignore | 3 +- tests/pom.xml | 194 +- .../config/DownloaderConfigBuilder.java | 114 - .../nzbhydra/config/IndexerConfigBuilder.java | 224 - .../tests/AbstractConfigReplacingTest.java | 50 - .../nzbhydra/tests/NzbDownloadingTests.java | 195 - .../nzbhydra/tests/NzbhydraMockMvcTest.java | 26 - ...ScreenshotTakingTestExecutionListener.java | 55 - .../tests/WebDriverConfiguration.java | 55 - .../tests/auth/HttpBasicAuthTest.java | 142 - .../nzbhydra/tests/pageobjects/CheckBox.java | 12 - .../tests/pageobjects/CheckboxFilter.java | 60 - .../tests/pageobjects/ColumnSortable.java | 81 - .../pageobjects/DropdownCheckboxButton.java | 126 - .../tests/pageobjects/FreetextFilter.java | 40 - .../nzbhydra/tests/pageobjects/ICheckBox.java | 8 - .../tests/pageobjects/ICheckboxFilter.java | 14 - .../tests/pageobjects/IColumnSortable.java | 12 - .../pageobjects/IDropdownCheckboxButton.java | 30 - .../tests/pageobjects/IFreetextFilter.java | 8 - .../org/nzbhydra/tests/pageobjects/ILink.java | 6 - .../tests/pageobjects/INumberRangeFilter.java | 8 - .../tests/pageobjects/ISelectionButton.java | 10 - .../nzbhydra/tests/pageobjects/IToggle.java | 9 - .../pageobjects/IndexerSelectionButton.java | 50 - .../org/nzbhydra/tests/pageobjects/Link.java | 11 - .../tests/pageobjects/NumberRangeFilter.java | 47 - .../nzbhydra/tests/pageobjects/SearchPO.java | 59 - .../tests/pageobjects/SearchResultsPO.java | 155 - .../tests/pageobjects/SelectionButton.java | 39 - .../nzbhydra/tests/pageobjects/Toggle.java | 21 - .../searching/ExternalApiEndToEndTest.java | 226 - .../ExternalApiSearchingIntegrationTest.java | 219 - .../SearchResultIdCalculatorTest.java | 56 - .../searching/SearchingIntegrationTest.java | 78 - .../searching/SearchingResultsUiTest.java | 529 - .../tests/searching/SearchingSystemTest.java | 68 - .../resources/config/application.properties | 24 - ...ictedWithBasicStatsAndAdminUser.properties | 22 - ...ictedWithBasicStatsAndAdminUser.properties | 14 - .../org/nzbhydra/tests/categories.properties | 4 - .../tests/downloadingTests.properties | 15 - .../tests/searching/fiveIndexers.json | 57 - .../nzbhydra/tests/searching/mockSabnzbd.json | 18 - .../nzbhydra/tests/searching/oneIndexer.json | 18 - .../tests/searching/simplesearchresult1a.xml | 27 - .../tests/searching/simplesearchresult1b.xml | 27 - .../tests/searching/simplesearchresult2.xml | 27 - .../threeIndexersForSearchInputTests.json | 44 - .../nzbhydra/tests/searching/twoIndexers.json | 32 - .../v1Migration/database/nzbhydra.mv.db | Bin 0 -> 36864 bytes .../instanceData/v1Migration/nzbhydra.yml | 475 + tests/system/pom.xml | 137 + .../src/main/java/org/nzbhydra/BeforeAll.java | 67 + .../main/java/org/nzbhydra/Downloader.java | 49 + .../main/java/org/nzbhydra/HydraClient.java | 132 + .../main/java/org/nzbhydra/HydraResponse.java | 80 + .../src/main/java/org/nzbhydra/Jackson.java | 78 + .../org/nzbhydra/SearchResultProvider.java | 45 + .../src/main/java/org/nzbhydra/Searcher.java | 56 + .../hydraconfigure/ConfigManager.java | 47 + .../hydraconfigure/DownloaderConfigurer.java | 58 + .../hydraconfigure/IndexerConfigurer.java | 77 + .../test/java/org/nzbhydra/BackupTest.java | 58 + .../java/org/nzbhydra/DebugInfosTest.java | 64 + .../test/java/org/nzbhydra/DockerConfig.java | 24 + .../java/org/nzbhydra/DownloaderTest.java | 89 + .../nzbhydra/ExternalApiSearchingTest.java | 71 + .../java/org/nzbhydra/ExternalToolsTest.java | 122 + .../java/org/nzbhydra/GenericStorageTest.java | 44 + .../test/java/org/nzbhydra/HistoryTest.java | 123 + .../src/test/java/org/nzbhydra/HydraPage.java | 36 + .../java/org/nzbhydra/IndexerWebTest.java | 82 + .../test/java/org/nzbhydra/MediaInfoTest.java | 63 + .../src/test/java/org/nzbhydra/NewsTest.java | 49 + .../java/org/nzbhydra/NotificationsTest.java | 73 + .../java/org/nzbhydra/NzbHandlingTest.java | 106 + .../src/test/java/org/nzbhydra/StatsTest.java | 83 + .../test/java/org/nzbhydra/TestConfig.java | 23 + .../resources/application-build.properties | 11 + .../src/test/resources/application.properties | 30 + .../src/test/resources/initialNzbhydra.yml | 477 + 1017 files changed, 102248 insertions(+), 12050 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/buildNative.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/system-test.yml create mode 100644 .github/workflows/test.yml create mode 100644 .run/NzbHydraNativeEntrypoint.run.xml create mode 100644 .snyk create mode 100644 buildCore.cmd create mode 100644 core/buildCoreAotOnly.cmd create mode 100644 core/runTracingAgent.cmd create mode 100644 core/src/main/java/org/nzbhydra/NativeHints.java create mode 100644 core/src/main/java/org/nzbhydra/NzbHydraNativeEntrypoint.java delete mode 100644 core/src/main/java/org/nzbhydra/WindowsTrayIcon.java create mode 100644 core/src/main/java/org/nzbhydra/backup/FailedBackupData.java delete mode 100644 core/src/main/java/org/nzbhydra/config/BaseConfig.java create mode 100644 core/src/main/java/org/nzbhydra/config/BaseConfigHandler.java delete mode 100644 core/src/main/java/org/nzbhydra/config/LoggingConfig.java delete mode 100644 core/src/main/java/org/nzbhydra/config/MainConfig.java delete mode 100644 core/src/main/java/org/nzbhydra/config/NotificationConfig.java delete mode 100644 core/src/main/java/org/nzbhydra/config/NotificationConfigEntry.java delete mode 100644 core/src/main/java/org/nzbhydra/config/SearchSourceRestriction.java delete mode 100644 core/src/main/java/org/nzbhydra/config/SearchingConfig.java delete mode 100644 core/src/main/java/org/nzbhydra/config/ValidatingConfig.java delete mode 100644 core/src/main/java/org/nzbhydra/config/auth/AuthConfig.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/AuthConfigValidator.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/BaseConfigValidator.java rename core/src/main/java/org/nzbhydra/config/{category/CategoriesConfig.java => validation/CategoriesConfigValidator.java} (62%) create mode 100644 core/src/main/java/org/nzbhydra/config/validation/ConfigValidationResult.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/ConfigValidationTools.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/ConfigValidator.java rename core/src/main/java/org/nzbhydra/config/{downloading/DownloaderConfig.java => validation/DownloaderConfigValidator.java} (50%) rename core/src/main/java/org/nzbhydra/config/{downloading/DownloadingConfig.java => validation/DownloadingConfigValidator.java} (51%) create mode 100644 core/src/main/java/org/nzbhydra/config/validation/IndexerConfigValidator.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/LoggingConfigValidator.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/MainConfigValidator.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/NotificationConfigValidator.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/SearchingConfigValidator.java create mode 100644 core/src/main/java/org/nzbhydra/config/validation/UserAuthConfigValidator.java create mode 100644 core/src/main/java/org/nzbhydra/database/DatabaseRecreationBean.java create mode 100644 core/src/main/java/org/nzbhydra/database/DatabaseRecreationConfig.java delete mode 100644 core/src/main/java/org/nzbhydra/database/FlywayMigration.java create mode 100644 core/src/main/java/org/nzbhydra/database/H2DialectExtended.java delete mode 100644 core/src/main/java/org/nzbhydra/database/migration/V2__MOVE_GENERIC_STORAGE.java delete mode 100644 core/src/main/java/org/nzbhydra/database/migration/V3__MOVE_GENERIC_STORAGE.java create mode 100644 core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderInstatiator.java delete mode 100644 core/src/main/java/org/nzbhydra/historystats/StatsResponse.java delete mode 100644 core/src/main/java/org/nzbhydra/historystats/stats/CountPerDayOfWeek.java rename core/src/main/java/org/nzbhydra/{searching/dtoseventsenums/DownloadType.java => mediainfo/AutocompleteType.java} (79%) delete mode 100644 core/src/main/java/org/nzbhydra/mediainfo/CustomTmdb.java create mode 100644 core/src/main/java/org/nzbhydra/problemdetection/DeleteOldDatabaseBackupDetector.java rename core/src/main/java/org/nzbhydra/searching/{CustomQueryAndTitleMapping.java => CustomQueryAndTitleMappingHandler.java} (63%) create mode 100644 core/src/main/java/org/nzbhydra/searching/IndexerInstantiator.java rename tests/src/test/java/org/nzbhydra/tests/pageobjects/IIndexerSelectionButton.java => core/src/main/java/org/nzbhydra/web/ApiError.java (69%) create mode 100644 core/src/main/java/org/nzbhydra/web/HydraErrorController.java create mode 100644 core/src/main/java/org/nzbhydra/web/HydraRestControllerAdvice.java delete mode 100644 core/src/main/java/org/nzbhydra/webaccess/AbstractAsyncClientHttpRequest.java delete mode 100644 core/src/main/java/org/nzbhydra/webaccess/AbstractBufferingAsyncClientHttpRequest.java delete mode 100644 core/src/main/java/org/nzbhydra/webaccess/OkHttp3AsyncClientHttpRequest.java create mode 100644 core/src/main/java/org/springframework/boot/SpringApplicationAotProcessor.java create mode 100644 core/src/main/resources/META-INF/native-image/jni-config.json create mode 100644 core/src/main/resources/META-INF/native-image/predefined-classes-config.json create mode 100644 core/src/main/resources/META-INF/native-image/proxy-config.json create mode 100644 core/src/main/resources/META-INF/native-image/reflect-config.json create mode 100644 core/src/main/resources/META-INF/native-image/resource-config.json create mode 100644 core/src/main/resources/META-INF/native-image/serialization-config.json create mode 100644 core/src/main/resources/changelog.yaml create mode 100644 core/src/main/resources/config/application-build.properties create mode 100644 core/src/main/resources/config/application-systemtest.properties delete mode 100644 core/src/main/resources/migration/V1.0__INITIAL.sql delete mode 100644 core/src/main/resources/migration/V1.10__ADD_INDEX_FOR_DOWNLOAD_STATUS_UPDATE_QUERIES.sql delete mode 100644 core/src/main/resources/migration/V1.11__REMOVE_INDEXERSTATUSES.sql delete mode 100644 core/src/main/resources/migration/V1.12__ADD_MEDIAINFO_UNIQUE.sql delete mode 100644 core/src/main/resources/migration/V1.13__EXTEND_GENERICSTORAGE_DATA_LENGTH.sql delete mode 100644 core/src/main/resources/migration/V1.14__SEARCHRESULT_LINK_LENGTH.sql delete mode 100644 core/src/main/resources/migration/V1.17__SHORTACCESS_AGAIN.sql delete mode 100644 core/src/main/resources/migration/V1.18__SEARCH_HISTORY_INDEXES.sql delete mode 100644 core/src/main/resources/migration/V1.19__ADD_TVINFO_IMDB_COLUMN.sql delete mode 100644 core/src/main/resources/migration/V1.1__NZBDOWNLOAD_USERAGENT.sql delete mode 100644 core/src/main/resources/migration/V1.20__EMPTY_TV_AND_MOVIE_INFO.sql delete mode 100644 core/src/main/resources/migration/V1.21__LINK_SEARCHENTITY_TO_INDEXERSEARCHENTITY.sql delete mode 100644 core/src/main/resources/migration/V1.22__INDEXERUNIQUENESSSCORE.sql delete mode 100644 core/src/main/resources/migration/V1.23__INDEXERUNIQUENESSSCORE_DELETEOLD.sql delete mode 100644 core/src/main/resources/migration/V1.24__REMOVE_OLD_SCORE_COLUMNS.sql delete mode 100644 core/src/main/resources/migration/V1.25__INDEXERSTATUS.sql delete mode 100644 core/src/main/resources/migration/V1.2__DISTINGUISH_USERNAME_IP.sql delete mode 100644 core/src/main/resources/migration/V1.3__DELETE_OLD_SEARCHRESULTS.sql delete mode 100644 core/src/main/resources/migration/V1.4__MAKE_SEARCHRESULT_INDEX_NON_UNIQUE.sql delete mode 100644 core/src/main/resources/migration/V1.5__MAKE_SEARCH_QUERY_COLUMN_BIGGER.sql delete mode 100644 core/src/main/resources/migration/V1.6__ADD_SEARCH_RESULT_TO_DOWNLOADS.sql delete mode 100644 core/src/main/resources/migration/V1.7__ADD_INDEXES_FOR_STATS.sql delete mode 100644 core/src/main/resources/migration/V1.8__INCREASE_QUERY_AND_ERROR_COLUMN_LENGTHs.sql delete mode 100644 core/src/main/resources/migration/V1.9__SET_DELETE_CASCADES.sql create mode 100644 core/src/main/resources/migration/V1__INITIAL.sql create mode 100644 core/src/main/resources/migration/V2__SEQUENCES.SQL delete mode 100644 core/src/main/resources/migration/V4__NOTIFICATION.sql delete mode 100644 core/src/main/resources/migration/V5__NOTIFICATION_ALLOW_NULL_URLS.sql delete mode 100644 core/src/main/resources/wrapperHashes.json create mode 100644 core/src/main/resources/wrapperHashes2.json create mode 100644 core/src/test/java/org/nzbhydra/NativeHintsTest.java create mode 100644 core/src/test/java/org/nzbhydra/downloading/FileDownloadEntityTest.java delete mode 100644 core/src/test/java/org/nzbhydra/downloading/NzbGetTest.java rename core/src/test/java/org/nzbhydra/mapping/{JackettCapsMappingTest.java => JackettCapsCustomQueryAndTitleMappingTest.java} (79%) rename core/src/test/java/org/nzbhydra/mapping/{RssCapsMappingTest.java => RssCapsCustomQueryAndTitleMappingTest.java} (68%) create mode 100644 core/src/test/java/org/nzbhydra/mapping/RssCustomQueryAndTitleMappingTest.java delete mode 100644 core/src/test/java/org/nzbhydra/mapping/RssMappingTest.java rename core/src/test/java/org/nzbhydra/mapping/{TorznabMappingTest.java => TorznabCustomQueryAndTitleMappingTest.java} (52%) rename core/src/test/java/org/nzbhydra/mapping/json/{JsonMappingTest.java => JsonCustomQueryAndTitleMappingTest.java} (73%) delete mode 100644 core/src/test/java/org/nzbhydra/mediainfo/CustomTmdbTest.java delete mode 100644 core/src/test/java/org/nzbhydra/mediainfo/TmdbHandlerTest.java create mode 100644 core/src/test/java/org/nzbhydra/notifications/NotificationEntityTest.java rename core/src/test/java/org/nzbhydra/searching/{CustomQueryAndTitleMappingTest.java => CustomQueryAndTitleCustomQueryAndTitleMappingHandlerTest.java} (60%) create mode 100644 docker/beta/.gitignore create mode 100644 docker/beta/dockerfile create mode 100644 docker/docker-compose.yaml create mode 100644 docker/nativeTest/.gitignore create mode 100644 docker/nativeTest/buildHydraDocker.sh create mode 100644 docker/nativeTest/dockerfile create mode 100644 docker/radarr/.gitignore create mode 100644 docker/radarr/data/config.xml create mode 100644 docker/radarr/docker-compose.yml create mode 100644 docker/sonarr/.gitignore create mode 100644 docker/sonarr/data/config.xml create mode 100644 docker/sonarr/docker-compose.yml create mode 100644 docker/waitForHealthyContainer.sh delete mode 100644 misc/build-and-release.cmd create mode 100644 misc/build-and-release.ps1 delete mode 100644 misc/copypoms.cmd delete mode 100644 misc/docker/.dockerignore delete mode 100644 misc/docker/.gitignore delete mode 100644 misc/docker/Dockerfile delete mode 100644 misc/docker/builddocker.cmd delete mode 100644 misc/docker/builddocker.sh delete mode 100644 misc/docker/docker-compose.yml delete mode 100644 misc/docker/notes delete mode 100644 misc/init.cmd create mode 100644 nativeNotes.md delete mode 100644 other/discordbot/pom.xml delete mode 100644 other/discordbot/src/main/java/org/nzbhydra/discordbot/Asset.java delete mode 100644 other/discordbot/src/main/java/org/nzbhydra/discordbot/DiscordBot.java delete mode 100644 other/discordbot/src/main/java/org/nzbhydra/discordbot/Release.java create mode 100644 other/github-release-plugin/src/main/java/org/nzbhydra/github/mavenreleaseplugin/PublishOnDiscordMojo.java create mode 100644 other/github-release-plugin/src/test/java/org/nzbhydra/github/mavenreleaseplugin/PublishOnDiscordMojoTest.java delete mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/changelog.json.orig create mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/changelog.yaml create mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/changelog.yaml.orig delete mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/changelogOld.json create mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/changelogOld.yaml create mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/discordPublisherPom.xml create mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/genericAsset.txt create mode 100644 other/github-release-plugin/src/test/resources/org/nzbhydra/github/mavenreleaseplugin/pomPrecheck.xml create mode 100644 other/loadtest/pom.xml create mode 100644 other/loadtest/src/main/java/org/nzbhydra/loadtest/GatlingRunner.java create mode 100644 other/loadtest/src/main/java/org/nzbhydra/loadtest/SearchSimulation.java create mode 100644 other/mockserver/src/main/resources/application-systemtest.properties rename other/wrapper/pyInstaller/{ => linux}/NZBHydra2.spec (100%) create mode 100644 other/wrapper/pyInstaller/windows/NzbHydra2.spec create mode 100644 other/wrapper/pyInstaller/windows/nzbhydra2wrapperWindows.py create mode 100644 other/wrapper/pyInstaller/windows/systemTray.py rename other/wrapper/pyInstaller/{ => windows_console}/NZBHydra2 Console.spec (100%) create mode 100644 releases/.gitignore create mode 100644 releases/generic-release/.gitignore create mode 100644 releases/generic-release/bin.xml create mode 100644 releases/generic-release/pom.xml create mode 100644 shared/assertions/pom.xml create mode 100644 shared/assertions/src/main/java/org/nzbhydra/AbstractGenericResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/AssertFactories.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/Assertions.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/GenericResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/SoftAssertions.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/backup/AbstractBackupEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/backup/BackupEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractBaseConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractHistoryUserInfoTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractLoggingConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractMainConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractNotificationConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractNotificationConfigEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractProxyTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractRestartRequiredAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractSearchSourceAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractSearchSourceRestrictionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/AbstractSearchingConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/BaseConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/HistoryUserInfoTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/LoggingConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/MainConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/NotificationConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/NotificationConfigEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/ProxyTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/RestartRequiredAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/SearchSourceAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/SearchSourceRestrictionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/SearchingConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/auth/AbstractAuthConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/auth/AbstractAuthTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/auth/AbstractUserAuthConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/auth/AuthConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/auth/AuthTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/auth/UserAuthConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/AbstractCategoriesConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/AbstractCategoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/AbstractNewznabCategoriesDeserializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/AbstractNewznabCategoriesSerializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/CategoriesConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/CategoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/NewznabCategoriesDeserializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/category/NewznabCategoriesSerializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/AbstractDownloadTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/AbstractDownloaderConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/AbstractDownloadingConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/AbstractFileDownloadAccessTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/AbstractNzbAddingTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/DownloadTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/DownloaderConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/DownloadingConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/FileDownloadAccessTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/downloading/NzbAddingTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/AbstractBackendTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/AbstractCapsCheckRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/AbstractCheckCapsResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/AbstractIndexerCategoryConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/AbstractIndexerConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/AbstractQueryFormatAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/AbstractSearchModuleTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/BackendTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/CapsCheckRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/CheckCapsResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/IndexerCategoryConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/IndexerConfigAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/QueryFormatAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/indexer/SearchModuleTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/mediainfo/AbstractMediaIdTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/mediainfo/MediaIdTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/notification/AbstractNotificationEventTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/notification/NotificationEventTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/searching/AbstractAffectedValueAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/searching/AbstractCustomQueryAndTitleMappingAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/searching/AbstractSearchTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/searching/AffectedValueAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/searching/CustomQueryAndTitleMappingAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/searching/SearchTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/sensitive/AbstractSensitiveDataAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/config/sensitive/SensitiveDataAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/AbstractAddFilesRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/AbstractDownloaderTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/AbstractFileDownloadEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/AbstractFileDownloadStatusAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/AbstractFileZipResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/AddFilesRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/DownloaderTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/FileDownloadEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/FileDownloadStatusAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/FileZipResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/AbstractAddNzbsResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/AbstractConvertersAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/AbstractDownloaderStatusAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/AddNzbsResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/ConvertersAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatusAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractAddNzbResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractCategoriesResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractHistoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractHistoryEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractHistoryResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractQueueAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractQueueEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractQueueResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AbstractStageLogEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AddNzbResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/CategoriesResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/HistoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/HistoryEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/HistoryResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/QueueAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/QueueEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/QueueResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/StageLogEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/externaltools/AbstractAddRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/externaltools/AddRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/AbstractFilterDefinitionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/AbstractFilterModelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/AbstractSortModelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/AbstractStatsResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/FilterDefinitionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/FilterModelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/SortModelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/StatsResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractAverageResponseTimeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractCountPerDayOfWeekAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractCountPerHourOfDayAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractDownloadOrSearchSharePerUserOrIpAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractDownloadPerAgeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractDownloadPerAgeStatsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractHistoryRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractIndexerApiAccessStatsEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractIndexerDownloadShareAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractIndexerScoreAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractStatsRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractSuccessfulDownloadsPerIndexerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AbstractUserAgentShareAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/AverageResponseTimeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/CountPerDayOfWeekAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/CountPerHourOfDayAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/DownloadOrSearchSharePerUserOrIpAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/DownloadPerAgeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/DownloadPerAgeStatsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/HistoryRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/IndexerApiAccessStatsEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/IndexerDownloadShareAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/IndexerScoreAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/StatsRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/SuccessfulDownloadsPerIndexerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/historystats/stats/UserAgentShareAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/indexers/AbstractIndexerEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/indexers/IndexerEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/AbstractSemanticVersionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/SemanticVersionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/changelog/AbstractChangelogChangeEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/changelog/AbstractChangelogVersionEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/changelog/ChangelogChangeEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/changelog/ChangelogVersionEntryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/github/AbstractAssetAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/github/AbstractReleaseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/github/AssetAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/github/ReleaseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/AbstractActionAttributeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/AbstractNewznabParametersAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/AbstractNewznabResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/AbstractOutputTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/ActionAttributeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/NewznabParametersAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/NewznabResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/OutputTypeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/builder/AbstractRssBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/builder/AbstractRssChannelBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/builder/AbstractRssItemBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/builder/RssBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/builder/RssChannelBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/builder/RssItemBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractJsonPubdateDeserializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractJsonPubdateSerializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonChannelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonChannelResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonEnclosureAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonEnclosureAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonErrorAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonImageAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonItemAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonItemAttrAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonItemAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonResponseAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/AbstractNewznabJsonRootAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/JsonPubdateDeserializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/JsonPubdateSerializerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonChannelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonChannelResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonEnclosureAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonEnclosureAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonErrorAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonImageAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonItemAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonItemAttrAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonItemAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonResponseAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/NewznabJsonRootAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonCategoriesHolderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonCategoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonCategoryAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonIdAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonLimitsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonLimitsAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonRegistrationAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonRegistrationAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonSearchIdAttributesHolderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonSearchingAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonServerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/AbstractCapsJsonServerAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonCategoriesHolderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonCategoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonCategoryAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonIdAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonLimitsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonLimitsAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonRegistrationAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonRegistrationAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonSearchIdAttributesHolderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonSearchingAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonServerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/json/caps/CapsJsonServerAttributesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/mock/AbstractNewznabMockBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/mock/AbstractNewznabMockRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/mock/NewznabMockBuilderAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/mock/NewznabMockRequestAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractJaxbPubdateAdapterAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabAttributeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlApilimitsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlChannelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlEnclosureAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlErrorAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlGuidAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlItemAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractNewznabXmlRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/AbstractXmlAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/JaxbPubdateAdapterAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabAttributeAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlApilimitsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlChannelAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlEnclosureAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlErrorAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlGuidAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlItemAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/NewznabXmlRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/XmlAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlCategoriesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlCategoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlLimitsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlRetentionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlSearchAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlSearchingAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/AbstractCapsXmlServerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlCategoriesAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlCategoryAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlLimitsAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlRetentionAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlSearchAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlSearchingAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/CapsXmlServerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/jackett/AbstractJacketCapsXmlIndexerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/jackett/AbstractJacketCapsXmlRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/jackett/JacketCapsXmlIndexerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mapping/newznab/xml/caps/jackett/JacketCapsXmlRootAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mediainfo/AbstractMediaInfoTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/mediainfo/MediaInfoTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/AbstractIndexerSearchEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/AbstractSearchResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/IndexerSearchEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/SearchResponseAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/db/AbstractIdentifierKeyValuePairTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/db/AbstractSearchEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/db/AbstractSearchResultEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/db/IdentifierKeyValuePairTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/db/SearchEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/db/SearchResultEntityTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/dtoseventsenums/AbstractIndexerSearchMetaDataAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/dtoseventsenums/AbstractSearchRequestParametersAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/dtoseventsenums/AbstractSearchResultWebTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchMetaDataAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchRequestParametersAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultWebTOAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/springnative/AbstractReflectionMarkerAssert.java create mode 100644 shared/assertions/src/main/java/org/nzbhydra/springnative/ReflectionMarkerAssert.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/GenericResponse.java (89%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/backup/BackupEntry.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/BaseConfig.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/HistoryUserInfoType.java (100%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/LoggingConfig.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/MainConfig.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/NotificationConfig.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/NotificationConfigEntry.java rename {core/src/main/java/org/nzbhydra/config/downloading => shared/mapping/src/main/java/org/nzbhydra/config}/ProxyType.java (86%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/RestartRequired.java (100%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/SearchSource.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/SearchSourceRestriction.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/SearchingConfig.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/auth/AuthConfig.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/auth/AuthType.java (100%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/auth/UserAuthConfig.java (54%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/category/CategoriesConfig.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/category/Category.java (66%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/category/NewznabCategoriesDeserializer.java (100%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/category/NewznabCategoriesSerializer.java (100%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/downloading/DownloadType.java (100%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/downloading/DownloaderConfig.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/downloading/DownloadingConfig.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/downloading/FileDownloadAccessType.java (100%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/downloading/NzbAddingType.java (100%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/indexer/BackendType.java rename {core/src/main/java/org/nzbhydra/indexers/capscheck => shared/mapping/src/main/java/org/nzbhydra/config/indexer}/CapsCheckRequest.java (74%) rename {core/src/main/java/org/nzbhydra/indexers/capscheck => shared/mapping/src/main/java/org/nzbhydra/config/indexer}/CheckCapsResponse.java (84%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/indexer/IndexerCategoryConfig.java (96%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/indexer/IndexerConfig.java (57%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/indexer/QueryFormat.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/indexer/SearchModuleType.java (100%) rename {core/src/main/java/org/nzbhydra => shared/mapping/src/main/java/org/nzbhydra/config}/mediainfo/MediaIdType.java (88%) rename {core/src/main/java/org/nzbhydra/notifications => shared/mapping/src/main/java/org/nzbhydra/config/notification}/NotificationEventType.java (88%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/searching/AffectedValue.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/config/searching/CustomQueryAndTitleMapping.java rename {core/src/main/java/org/nzbhydra/searching/dtoseventsenums => shared/mapping/src/main/java/org/nzbhydra/config/searching}/SearchType.java (80%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/config/sensitive/SensitiveData.java (100%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/AddFilesRequest.java (87%) rename {core/src/main/java/org/nzbhydra/config => shared/mapping/src/main/java/org/nzbhydra}/downloading/DownloaderType.java (86%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/downloading/FileDownloadEntityTO.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/FileDownloadStatus.java (87%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/FileZipResponse.java (93%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/AddNzbsResponse.java (94%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/Converters.java (100%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatus.java (96%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/AddNzbResponse.java (93%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/CategoriesResponse.java (92%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/History.java (94%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/HistoryEntry.java (96%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/HistoryResponse.java (91%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/Queue.java (96%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/QueueEntry.java (95%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/QueueResponse.java (91%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/mapping/StageLogEntry.java (93%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/externaltools/AddRequest.java (93%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/FilterDefinition.java (81%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/FilterModel.java (100%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/SortModel.java (78%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/historystats/StatsResponse.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/AverageResponseTime.java (87%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/historystats/stats/CountPerDayOfWeek.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/CountPerHourOfDay.java (80%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/DownloadOrSearchSharePerUserOrIp.java (81%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/DownloadPerAge.java (79%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/DownloadPerAgeStats.java (83%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/HistoryRequest.java (87%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/IndexerApiAccessStatsEntry.java (81%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/IndexerDownloadShare.java (81%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/IndexerScore.java (83%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/StatsRequest.java (95%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/SuccessfulDownloadsPerIndexer.java (84%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/historystats/stats/UserAgentShare.java (86%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/indexers/IndexerEntityTO.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/mediainfo/MediaInfoTO.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/news/NewsEntryForWeb.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/notifications/NotificationEntityTO.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/notifications/NotificationMessageType.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/searching/IndexerSearchEntityTO.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/searching/SearchResponse.java (92%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/searching/db/IdentifierKeyValuePairTO.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/searching/db/SearchEntityTO.java create mode 100644 shared/mapping/src/main/java/org/nzbhydra/searching/db/SearchResultEntityTO.java rename {core => shared/mapping}/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchMetaData.java (94%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchRequestParameters.java (95%) rename {core => shared/mapping}/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultWebTO.java (93%) create mode 100644 shared/mapping/src/main/java/org/nzbhydra/springnative/ReflectionMarker.java delete mode 100644 tests/src/main/java/org/nzbhydra/config/DownloaderConfigBuilder.java delete mode 100644 tests/src/main/java/org/nzbhydra/config/IndexerConfigBuilder.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/AbstractConfigReplacingTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/NzbDownloadingTests.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/NzbhydraMockMvcTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/ScreenshotTakingTestExecutionListener.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/WebDriverConfiguration.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/auth/HttpBasicAuthTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/CheckBox.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/CheckboxFilter.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/ColumnSortable.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/DropdownCheckboxButton.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/FreetextFilter.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/ICheckBox.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/ICheckboxFilter.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/IColumnSortable.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/IDropdownCheckboxButton.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/IFreetextFilter.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/ILink.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/INumberRangeFilter.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/ISelectionButton.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/IToggle.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/IndexerSelectionButton.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/Link.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/NumberRangeFilter.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/SearchPO.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/SearchResultsPO.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/SelectionButton.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/pageobjects/Toggle.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/searching/ExternalApiEndToEndTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/searching/ExternalApiSearchingIntegrationTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/searching/SearchResultIdCalculatorTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/searching/SearchingIntegrationTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/searching/SearchingResultsUiTest.java delete mode 100644 tests/src/test/java/org/nzbhydra/tests/searching/SearchingSystemTest.java delete mode 100644 tests/src/test/resources/config/application.properties delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/auth/allRestrictedWithBasicStatsAndAdminUser.properties delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/auth/onlyConfigRestrictedWithBasicStatsAndAdminUser.properties delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/categories.properties delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/downloadingTests.properties delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/fiveIndexers.json delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/mockSabnzbd.json delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/oneIndexer.json delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/simplesearchresult1a.xml delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/simplesearchresult1b.xml delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/simplesearchresult2.xml delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/threeIndexersForSearchInputTests.json delete mode 100644 tests/src/test/resources/org/nzbhydra/tests/searching/twoIndexers.json create mode 100644 tests/system/instanceData/v1Migration/database/nzbhydra.mv.db create mode 100644 tests/system/instanceData/v1Migration/nzbhydra.yml create mode 100644 tests/system/pom.xml create mode 100644 tests/system/src/main/java/org/nzbhydra/BeforeAll.java create mode 100644 tests/system/src/main/java/org/nzbhydra/Downloader.java create mode 100644 tests/system/src/main/java/org/nzbhydra/HydraClient.java create mode 100644 tests/system/src/main/java/org/nzbhydra/HydraResponse.java create mode 100644 tests/system/src/main/java/org/nzbhydra/Jackson.java create mode 100644 tests/system/src/main/java/org/nzbhydra/SearchResultProvider.java create mode 100644 tests/system/src/main/java/org/nzbhydra/Searcher.java create mode 100644 tests/system/src/main/java/org/nzbhydra/hydraconfigure/ConfigManager.java create mode 100644 tests/system/src/main/java/org/nzbhydra/hydraconfigure/DownloaderConfigurer.java create mode 100644 tests/system/src/main/java/org/nzbhydra/hydraconfigure/IndexerConfigurer.java create mode 100644 tests/system/src/test/java/org/nzbhydra/BackupTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/DebugInfosTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/DockerConfig.java create mode 100644 tests/system/src/test/java/org/nzbhydra/DownloaderTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/ExternalApiSearchingTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/ExternalToolsTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/GenericStorageTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/HistoryTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/HydraPage.java create mode 100644 tests/system/src/test/java/org/nzbhydra/IndexerWebTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/MediaInfoTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/NewsTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/NotificationsTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/NzbHandlingTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/StatsTest.java create mode 100644 tests/system/src/test/java/org/nzbhydra/TestConfig.java create mode 100644 tests/system/src/test/resources/application-build.properties create mode 100644 tests/system/src/test/resources/application.properties create mode 100644 tests/system/src/test/resources/initialNzbhydra.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index de9ca4af0..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Java Maven CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-java/ for more details -# -version: 2 -jobs: - build: - docker: - - image: circleci/openjdk:10-jdk-browsers - - working_directory: ~/repo - - environment: - - MAVEN_OPTS: -Xmx3200m - - PHANTOMJSBIN: /usr/local/bin/phantomjs - - steps: - - checkout - - - restore_cache: - keys: - - v1-dependencies-{{ checksum "pom.xml" }} - - v1-dependencies- - - - run: mkdir -p ~/junit/ - - #Clean install all but without integration tests - - run: mvn -T 2 -pl "!org.nzbhydra:tests,!org.nzbhydra:linux-release,!org.nzbhydra:windows-release" clean install dependency:resolve-plugins dependency:go-offline - - run: find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/junit/ \; - - run: find . -type f -regex ".*/target/screenshots/.*png" -exec cp {} ~/junit/ \; - - - # Integration tests produce weird transaction errors when run with maven :-/ - # #Run integration tests - # - run: mvn -f tests/pom.xml test - # - run: find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/junit/ \; - # - run: find . -type f -regex ".*/target/screenshots/.*png" -exec cp {} ~/junit/ \; - - #Build releases - # - run: mkdir -p ~/releases/ - # - run: mvn -T 2 -pl "org.nzbhydra:linux-release,org.nzbhydra:windows-release" clean install dependency:resolve-plugins dependency:go-offline - # - run: cp releases/linux-release/target/*.zip ~/releases/ - # - run: cp releases/windows-release/target/*.zip ~/releases/ - - #Collect all surefire reports and put them in one folder for Circle to parse - - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "pom.xml" }} - - - store_test_results: - path: ~/junit -# - store_artifacts: -# path: ~/junit -# - store_artifacts: -# path: ~/releases - -#workflows didn't work because I cached the m2 cache in the build step and then restored it in the test step. without a changed pom.xml old JARs would be used -#possible fix: another more specific save_cache for ~/.m2/repository/org/nzbhydra/ \ No newline at end of file diff --git a/.github/workflows/buildNative.yml b/.github/workflows/buildNative.yml new file mode 100644 index 000000000..2eb8b9481 --- /dev/null +++ b/.github/workflows/buildNative.yml @@ -0,0 +1,112 @@ +name: Native Build + +on: + workflow_dispatch: + workflow_call: + +jobs: + build: + strategy: + matrix: + os: [ ubuntu-20.04 ] + fail-fast: false + env: + HYDRA_NATIVE_BUILD: true + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + # Check out last 15 commits + fetch-depth: 15 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + cache: 'maven' + + - name: "Get changed files in core module" + id: changed-files-specific + uses: tj-actions/changed-files@v35 + with: + since_last_remote_commit: true + files: | + core/** + + - if: steps.changed-files-specific.outputs.any_changed == 'true' + run: | + echo "::notice::Will build new native image / docker container. Changed files in core since last push:" + echo "${{ steps.changed-files-specific.outputs.all_changed_files }}" + + - if: steps.changed-files-specific.outputs.any_changed == 'false' + run: | + echo "::notice::Will skip build of native image / docker container. No changed files in core since last push." + + - name: NativeImage + uses: graalvm/setup-graalvm@v1 + if: steps.changed-files-specific.outputs.any_changed == 'true' + with: + java-version: '17' + version: 'latest' + components: 'native-image' + cache: 'maven' + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: "Install all with Maven" + + run: mvn --batch-mode clean install -DskipTests -T 1C + - name: "Run unit tests" + run: mvn --batch-mode test -T 1C -pl !org.nzbhydra:tests,!org.nzbhydra.tests:system --fail-at-end + + - name: "Create test Report" + uses: dorny/test-reporter@v1 + if: always() + continue-on-error: true + with: + name: Unit test report + path: "**/surefire-reports/*.xml" + reporter: java-junit + + - name: "Build native image" + if: steps.changed-files-specific.outputs.any_changed == 'true' + working-directory: ./core + run: | + mvn --batch-mode -Pnative clean native:compile -DskipTests + + - name: "UPX linux artifact" + if: steps.changed-files-specific.outputs.any_changed == 'true' + uses: crazy-max/ghaction-upx@v2 + with: + files: core/target/core + args: -q + + - name: "Upload linux artifact" + if: steps.changed-files-specific.outputs.any_changed == 'true' + uses: actions/upload-artifact@v3 + with: + name: coreLinux + path: core/target/core + + - name: "Copy artifact to include folder" + if: steps.changed-files-specific.outputs.any_changed == 'true' + run: | + mv core/target/core ./docker/nativeTest/ + chmod +x ./docker/nativeTest/core + + - name: "Login to GitHub Container Registry" + if: steps.changed-files-specific.outputs.any_changed == 'true' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: "Build core and push container" + if: steps.changed-files-specific.outputs.any_changed == 'true' + run: | + cp other/wrapper/nzbhydra2wrapperPy3.py ./docker/nativeTest/ + cd ./docker/nativeTest/ + docker build -t hydradocker . + docker tag hydradocker:latest ghcr.io/theotherp/hydradocker:latest + docker push ghcr.io/theotherp/hydradocker:latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..176b8363d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + workflow_dispatch: + inputs: + releaseVersion: + required: true + type: string + description: "Version to be released, like 1.2.3" + nextVersion: + required: true + type: string + description: "Version to be set afterwards, like 1.2.4-SNAPSHOT" + dryRun: + required: true + default: true + type: boolean + description: "Uncheck to actually execute the release" + selfHostedRunner: + required: true + default: false + type: boolean + description: "Has no effect, just as a reminder that the self-hosted windows runner must be running" + +jobs: + build: + uses: ./.github/workflows/buildNative.yml + release: + needs: [build] + runs-on: ubuntu-latest + env: + githubReleasesUrl: https://api.github.com/repos/{{github.repository}}/releases + steps: + - uses: actions/checkout@v3 + name: "Check out source" + - name: "Display structure of working directory" + run: ls . + - uses: actions/download-artifact@v3 + name: "Download native artifacts" + with: + path: ~/artifacts + - name: "Display structure of artifacts folder" + run: ls -R ~/artifacts + - name: "Copy artifacts to include folders" + run: | + mv ~/artifacts/coreLinux/* ./releases/linux-release/include/ + chmod +x ./releases/linux-release/include/nzbhydra2 + mv ~/artifacts/coreWindows/* ./releases/windows-release/include/ + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + cache: 'maven' + - name: "Run release script" + run: | + misc/build-and-release.sh ${{ github.event.inputs.releaseVersion }} ${{ github.event.inputs.nextVersion }} ${{ github.event.inputs.dryRun }} diff --git a/.github/workflows/system-test.yml b/.github/workflows/system-test.yml new file mode 100644 index 000000000..a93e87770 --- /dev/null +++ b/.github/workflows/system-test.yml @@ -0,0 +1,130 @@ +name: system-test + +on: + push: + workflow_dispatch: + +jobs: + waitForNative: + uses: ./.github/workflows/buildNative.yml + buildMockserver: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + name: "Check out source" + with: + # Check out last 15 commits + fetch-depth: 15 + + - name: "Get changed files in core module" + id: changed-files-specific + uses: tj-actions/changed-files@v35 + with: + since_last_remote_commit: true + files: | + other/mockserver/** + + - if: steps.changed-files-specific.outputs.any_changed == 'true' + run: | + echo "::notice::Will build new mock server container. Changed files since last push:" + echo "${{ steps.changed-files-specific.outputs.all_changed_files }}" + + - if: steps.changed-files-specific.outputs.any_changed == 'false' + run: | + echo "::notice::Will skip build of new mock server container. No changed files since last push." + + - name: Set up JDK 17 + if: steps.changed-files-specific.outputs.any_changed == 'true' + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + cache: 'maven' + + - name: "Login to GitHub Container Registry" + if: steps.changed-files-specific.outputs.any_changed == 'true' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: "Build and push mockserver container" + if: steps.changed-files-specific.outputs.any_changed == 'true' + run: | + mvn --batch-mode install -DskipTests -T 1C + cd other/mockserver/ + mvn --batch-mode spring-boot:build-image + docker tag mockserver:3.0.0 ghcr.io/theotherp/mockserver:3.0.0 + docker push ghcr.io/theotherp/mockserver:3.0.0 + + runSystemTests: + needs: [ waitForNative, buildMockserver ] + runs-on: ubuntu-latest + strategy: + matrix: + test: [ { port: 5076, name: core }, { port: 5077, name: v1Migration } ] + env: + spring_profiles_active: build,systemtest + nzbhydra_port: ${{ matrix.test.port }} + nzbhydra_name: ${{ matrix.test.name }} + nzbhydra_host_external: http://${{ matrix.test.name }}:5076 + nzbhydra.host.external: http://${{ matrix.test.name }}:5076 + steps: + - run: echo Running test ${{ matrix.test.name }} with port ${{ matrix.test.port }} + - uses: actions/checkout@v3 + name: "Check out source" + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + cache: 'maven' + + - name: "Install" + run: mvn --batch-mode clean install -DskipTests -pl org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:assertions + + - name: "Create docker network" + run: docker network create systemtest + + - name: "Copy v1Migration docker data" + run: | + mkdir -p /tmp/hydra/v1MigrationDataFolder + cp -R tests/system/instanceData/v1Migration/* /tmp/hydra/v1MigrationDataFolder/ + + - name: "Run docker compose" + run: | + cd docker + docker-compose up -d + + - name: "Wait for healthy containers" + run: | + docker ps + sleep 10 + docker ps + sleep 10 + docker ps + echo "Core container mounts:" + docker container inspect -f '{{ .Mounts}}' core + echo "v1Migration container mounts:" + docker container inspect -f '{{ .Mounts}}' v1Migration + + - name: "Run tests" + run: mvn --batch-mode test -pl org.nzbhydra.tests:system -DtrimStackTrace=false + + - name: "Upload data folder artifact" + uses: actions/upload-artifact@v3 + if: always() + with: + name: data + path: /tmp/hydra + + - name: "Create test Report" + uses: dorny/test-reporter@v1 + if: always() + continue-on-error: true + with: + name: System test report ${{ matrix.test.name }} + path: "**/surefire-reports/*.xml" + reporter: java-junit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..181f0bc0d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Java CI + +on: [ workflow_dispatch ] + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + cache: 'maven' + - name: Test with Maven + run: mvn --batch-mode --update-snapshots verify diff --git a/.gitignore b/.gitignore index b522f368c..b71a831e8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,11 @@ pom.xml.versionsBackup javacore* heapdump* /ui +*token* !/.idea/compiler.xml !/.idea/vcs.xml !/.idea/misc.xml misc/rsyncToServers.sh /nzbhydra.yml +/results diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b3c907d59..a184642c9 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -22,7 +22,9 @@ - + + + - + \ No newline at end of file diff --git a/.run/NzbHydraNativeEntrypoint.run.xml b/.run/NzbHydraNativeEntrypoint.run.xml new file mode 100644 index 000000000..210d296a2 --- /dev/null +++ b/.run/NzbHydraNativeEntrypoint.run.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/.snyk b/.snyk new file mode 100644 index 000000000..7f6d55440 --- /dev/null +++ b/.snyk @@ -0,0 +1,9 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.25.0 +ignore: + SNYK-JAVA-ORGYAML-3152153: + - '*': + reason: no exploit + expires: 2030-04-01T00:00:00.000Z + created: 2023-01-20T10:45:42.937Z +patch: {} diff --git a/buildCore.cmd b/buildCore.cmd new file mode 100644 index 000000000..98f37476a --- /dev/null +++ b/buildCore.cmd @@ -0,0 +1,12 @@ +@echo off + +setlocal + +call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 + +set path=c:\Programme\graalvm-ce-java17-22.2.0\bin\;%PATH%;c:\Programme\graalvm-ce-java17-22.2.0\bin\ +set java_home=c:\Programme\graalvm-ce-java17-22.2.0\ +set HYDRA_NATIVE_BUILD=true +call mvn -pl org.nzbhydra:core -Pnative clean native:compile -DskipTests + +endlocal diff --git a/changelog.md b/changelog.md index 62ea77bc5..fb3719877 100644 --- a/changelog.md +++ b/changelog.md @@ -48,13 +48,15 @@ ### v4.7.0 BETA (2022-09-18) -**Feature** Use custom mappings to transform indexer result titles. Use this to clean up titles, add season or episode to it or whatever. See #794 +**Feature** Use custom customQueryAndTitleMappings to transform indexer result titles. Use this to clean up titles, add season or episode to it or whatever. See #794 -**Fix** Some of you have an instance running which is exposed to the internet, without any authentication method. I previously tried to recognize this by some heuristic which was a bit naive and caused a lot of false positives. NZBHydra will now periodically try to determine your public IP and actually check if the used port is open. This might still not always work (e.g. in when you're running it using a VPN in which case I guess you know what're doing. Ultimately it's up to you to get your shit together. +**Fix** Some of you have an instance running which is exposed to the internet, without any authentication method. I previously tried to recognize this by some heuristic which was a bit naive and caused a lot of false positives. NZBHydra +will now periodically try to determine your public IP and actually check if the used port is open. This might still not always work (e.g. in when you're running it using a VPN in which case I guess you know what're doing. Ultimately it's up +to you to get your shit together. **Fix** Only warn about settings violating indexers' rules if the indexers are actually enabled. -**Fix** Fix saving config with custom mappings. +**Fix** Fix saving config with custom customQueryAndTitleMappings. @@ -74,7 +76,7 @@ **Feature** Automatically use NZB access and adding types required by certain indexers. See #784. -**Feature** Add debug logging for category mapping. +**Feature** Add debug logging for category customQueryAndTitleMapping. @@ -100,13 +102,13 @@ **Fix** Add the current API hit to the number of reported API hits in response. -**Fix** Fix name of logging marker "Custom mapping" (was "Config mapping"). +**Fix** Fix name of logging marker "Custom customQueryAndTitleMapping" (was "Config customQueryAndTitleMapping"). ### v4.3.2 (2022-06-13) -**Fix** Fix use of groups in custom search request mapping. See #700 +**Fix** Fix use of groups in custom search request customQueryAndTitleMapping. See #700 **Fix** Fix download of backup files. See #772 @@ -314,7 +316,8 @@ ### v3.14.0 (2021-04-11) -**Feature** Custom mapping for queries and titles. This allows you to customize / change the values used by external tools or returned by metadata providers like TVDB. See #700. +**Feature** Custom customQueryAndTitleMapping for queries and titles. This allows you to customize / change the values used by external tools or returned by metadata providers like TVDB. +See #700. @@ -1366,7 +1369,8 @@ ### v2.9.5 (2019-11-23) -**Feature** I realised the indexer score is too complex to show in a chart and replaced it with a table, that shows more information. It will now contain the average uniqueness score, the number of unique downloads and the number of searches which resulted in a download and where an indexer was involved. +**Feature** I realised the indexer score is too complex to show in a chart and replaced it with a table, that shows more information. It will now contain the average uniqueness score, the number of unique downloads and the number of +searches which resulted in a download and where an indexer was involved. @@ -1960,7 +1964,7 @@ ### v2.1.7 (2018-12-30) -**Fix** Fix/improve category mapping introduced in 2.1.6. Use custom newznab categories if none from the predefined range are provided. +**Fix** Fix/improve category customQueryAndTitleMapping introduced in 2.1.6. Use custom newznab categories if none from the predefined range are provided. @@ -1968,7 +1972,7 @@ **Fix** When uploading a backup file the UI didn't update to inform the user about the progress after the file was uploaded. -**Fix** Improve category mapping for (torznab) indexers. Some use custom newznab category numbers (>9999) which could not be properly mapped to preconfigured categories. +**Fix** Improve category customQueryAndTitleMapping for (torznab) indexers. Some use custom newznab category numbers (>9999) which could not be properly mapped to preconfigured categories. @@ -2178,7 +2182,7 @@ **Fix** Restoring from web UI had no effect -**Fix** Category mapping would sometimes not work for incoming searches +**Fix** Category customQueryAndTitleMapping would sometimes not work for incoming searches diff --git a/core/.gitignore b/core/.gitignore index 0eb5d5f3e..2c8d4f939 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -22,4 +22,5 @@ sql notes.sql /logback-access.xml *.dmp *.trc -package-lock.json \ No newline at end of file +package-lock.json +afile.txt diff --git a/core/buildCoreAotOnly.cmd b/core/buildCoreAotOnly.cmd new file mode 100644 index 000000000..c7e70d8ff --- /dev/null +++ b/core/buildCoreAotOnly.cmd @@ -0,0 +1,12 @@ +@echo off + +setlocal + +call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 + +set path=c:\Programme\graalvm-ce-java17-22.2.0\bin\;%PATH%;c:\Programme\graalvm-ce-java17-22.2.0\bin\ +set java_home=c:\Programme\graalvm-ce-java17-22.2.0\ +set HYDRA_NATIVE_BUILD=true +mvn -Pnative clean package -DskipTests + +endlocal diff --git a/core/pom.xml b/core/pom.xml index 0bf087daa..593c4f267 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -5,7 +5,7 @@ org.nzbhydra nzbhydra2 - 4.7.7-SNAPSHOT + 5.0.0-SNAPSHOT core @@ -14,6 +14,7 @@ ${maven.build.timestamp} yyyy-MM-dd HH:mm + 2.28.0.Final @@ -33,6 +34,16 @@ + + + + org.graalvm.buildtools + native-maven-plugin + 0.9.19 + true + + + org.apache.maven.plugins @@ -46,6 +57,8 @@ TheOtherP + ${project.version} + ${maven.build.timestamp} @@ -53,35 +66,30 @@ - - org.apache.maven.plugins - maven-compiler-plugin - ${maven.compiler.plugin.version} - - 1.8 - 1.8 - - + org.springframework.boot spring-boot-maven-plugin - ${spring.boot.version} + ${spring.boot.maven.version} repackage - exec + org.nzbhydra.NzbHydra - org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M2 + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 @@ -91,7 +99,7 @@ org.nzbhydra mapping - 4.7.7-SNAPSHOT + 5.0.0-SNAPSHOT @@ -163,13 +171,18 @@ spring-boot-starter-actuator ${spring.boot.version} + + org.glassfish.expressly + expressly + 5.0.0 + com.h2database h2 - 1.4.200 + 2.1.214 com.github.marschall @@ -197,7 +210,7 @@ com.fasterxml.jackson.core jackson-annotations - 2.13.0 + ${jackson.version} com.fasterxml.jackson.datatype @@ -217,18 +230,19 @@ org.apache.logging.log4j log4j-api - 2.17.2 + 2.19.0 org.projectlombok lombok ${lombok.version} - true + compile + com.google.guava guava - 20.0 + 31.1-jre commons-io @@ -240,11 +254,6 @@ commons-lang3 ${commons-lang3.version} - - com.uwetrottmann.tmdb2 - tmdb-java - 1.6.0 - com.github.briandilley.jsonrpc4j jsonrpc4j @@ -265,9 +274,9 @@ - com.vladsch.flexmark - flexmark - 0.34.12 + org.commonmark + commonmark + 0.20.0 com.squareup.okhttp3 @@ -277,7 +286,7 @@ com.squareup.okhttp3 logging-interceptor - 4.9.3 + ${okhttp.version} @@ -289,7 +298,7 @@ org.jsoup jsoup - 1.11.3 + 1.15.3 net.jodah @@ -301,54 +310,42 @@ URISchemeHandler ${uri-scheme-handler.version} - - com.sun.activation - javax.activation - 1.2.0 - com.github.ben-manes.caffeine caffeine - 2.6.2 + 3.1.2 - net.jodah + dev.failsafe failsafe - 1.1.1 + 3.3.0 joda-time joda-time - 2.10.14 + 2.12.2 compile org.hibernate.validator hibernate-validator - 6.1.5.Final + 8.0.0.Final org.javers javers-core - 6.6.5 + ${javers-core.version} - - - - org.springframework.boot - spring-boot-devtools - ${spring.boot.version} - true - - - org.springdoc - springdoc-openapi-ui - - 1.6.9 - + + + + + + + @@ -366,11 +363,12 @@ logback-access ${logback.version} - - net.rakugakibox.spring.boot - logback-access-spring-boot-starter - 2.7.1 - + + + + + + net.logstash.logback logstash-logback-encoder @@ -385,17 +383,17 @@ org.slf4j slf4j-api - 1.7.36 + 2.0.5 org.nzbhydra sockslib - 1.0.0 + 3.0.0 net.sourceforge.htmlunit htmlunit - 2.64.0 + 2.67.0 @@ -420,13 +418,13 @@ org.mockito mockito-core - 4.5.1 + 4.8.1 test org.hamcrest hamcrest-library - 1.3 + 2.2 test @@ -442,13 +440,139 @@ - junit - junit - ${junit.version} + org.jboss.forge.roaster + roaster-api + ${version.roaster} + test + + + org.jboss.forge.roaster + roaster-jdt + ${version.roaster} test + + + dev + + + + org.springframework.boot + spring-boot-devtools + ${spring.boot.devtools.version} + true + + + + + native + + org.nzbhydra.NzbHydra + + + + + + org.springframework.boot + spring-boot-maven-plugin + + org.nzbhydra.NzbHydra + + paketobuildpacks/builder:tiny + + true + + + + + + process-aot + + process-aot + + + + + + org.graalvm.buildtools + native-maven-plugin + + + -H:+ReportExceptionStackTraces + -H:-DeadlockWatchdogExitOnTimeout + -H:DeadlockWatchdogInterval=0 + --initialize-at-build-time=org.apache.commons.logging.LogFactoryService + + ${project.build.outputDirectory} + + true + + 22.3 + + + + add-reachability-metadata + + add-reachability-metadata + + + + + + + + + + nativeTest + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + process-test-aot + + process-test-aot + + + + + + org.graalvm.buildtools + native-maven-plugin + + ${project.build.outputDirectory} + + true + + 22.3 + + + + native-test + + test + + + + + + + + + + diff --git a/core/runTracingAgent.cmd b/core/runTracingAgent.cmd new file mode 100644 index 000000000..fe7ef2114 --- /dev/null +++ b/core/runTracingAgent.cmd @@ -0,0 +1,13 @@ +@echo off + +setlocal + +call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 + +set path=c:\Programme\graalvm-ce-java17-22.2.0\bin\;%PATH%;c:\Programme\graalvm-ce-java17-22.2.0\bin\ +set java_home=c:\Programme\graalvm-ce-java17-22.2.0\ +set HYDRA_NATIVE_BUILD=true +cd target +java -DspringAot=true -agentlib:native-image-agent=config-output-dir=hints -jar core-5.0.0-SNAPSHOT-exec.jar directstart + +endlocal diff --git a/core/src/main/java/org/nzbhydra/DevEndpoint.java b/core/src/main/java/org/nzbhydra/DevEndpoint.java index 018597082..d76819f3c 100644 --- a/core/src/main/java/org/nzbhydra/DevEndpoint.java +++ b/core/src/main/java/org/nzbhydra/DevEndpoint.java @@ -16,6 +16,8 @@ package org.nzbhydra; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.externaltools.AddRequest; @@ -25,14 +27,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.annotation.Secured; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import java.math.BigInteger; import java.util.List; @@ -58,6 +60,18 @@ public class DevEndpoint { return resultList.get(0); } + @Secured({"ROLE_ADMIN"}) + @RequestMapping(value = "/dev/throwException", method = RequestMethod.GET) + public BigInteger throwException() throws Exception { + throw new RuntimeException("test"); + } + + @Secured({"ROLE_ADMIN"}) + @RequestMapping(value = "/dev/throwAccessDeniedException", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public BigInteger throwAccessDeniedException() throws Exception { + throw new AccessDeniedException("test"); + } + @Secured({"ROLE_ADMIN"}) @Transactional @RequestMapping(value = "/dev/deleteDanglingIndexersearches", method = RequestMethod.GET) diff --git a/core/src/main/java/org/nzbhydra/ExceptionInfo.java b/core/src/main/java/org/nzbhydra/ExceptionInfo.java index 05344fd67..9061625d3 100644 --- a/core/src/main/java/org/nzbhydra/ExceptionInfo.java +++ b/core/src/main/java/org/nzbhydra/ExceptionInfo.java @@ -1,10 +1,12 @@ package org.nzbhydra; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.time.Instant; @Data +@ReflectionMarker public class ExceptionInfo { private long timestamp; private int status; diff --git a/core/src/main/java/org/nzbhydra/InstanceCounter.java b/core/src/main/java/org/nzbhydra/InstanceCounter.java index 4b19b4ee3..7d534b817 100644 --- a/core/src/main/java/org/nzbhydra/InstanceCounter.java +++ b/core/src/main/java/org/nzbhydra/InstanceCounter.java @@ -16,6 +16,8 @@ package org.nzbhydra; +import jakarta.annotation.PostConstruct; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; import org.slf4j.Logger; @@ -26,7 +28,6 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import java.io.IOException; import java.net.URI; @@ -42,6 +43,8 @@ public class InstanceCounter { @Autowired private ConfigProvider configProvider; + @Autowired + private BaseConfigHandler baseConfigHandler; @PostConstruct public void downloadInstanceCounter() { @@ -51,7 +54,7 @@ public class InstanceCounter { if (response.getStatusCode().is2xxSuccessful()) { logger.info("Instance counted"); configProvider.getBaseConfig().getMain().setInstanceCounterDownloaded(true); - configProvider.getBaseConfig().save(false); + baseConfigHandler.save(false); } else { logger.error("Unable to count instance. Response: " + response.getStatusText()); } diff --git a/core/src/main/java/org/nzbhydra/Markdown.java b/core/src/main/java/org/nzbhydra/Markdown.java index 754086ed9..6dc4b1434 100644 --- a/core/src/main/java/org/nzbhydra/Markdown.java +++ b/core/src/main/java/org/nzbhydra/Markdown.java @@ -1,17 +1,16 @@ package org.nzbhydra; -import com.vladsch.flexmark.ast.Node; -import com.vladsch.flexmark.html.HtmlRenderer; -import com.vladsch.flexmark.parser.Parser; -import com.vladsch.flexmark.util.options.MutableDataSet; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; public class Markdown { public static String renderMarkdownAsHtml(String markdown) { - MutableDataSet options = new MutableDataSet(); - Parser parser = Parser.builder(options).build(); - HtmlRenderer renderer = HtmlRenderer.builder(options).build(); + Parser parser = Parser.builder().build(); Node document = parser.parse(markdown); + HtmlRenderer renderer = HtmlRenderer.builder().build(); return renderer.render(document); } diff --git a/core/src/main/java/org/nzbhydra/NativeHints.java b/core/src/main/java/org/nzbhydra/NativeHints.java new file mode 100644 index 000000000..042abff9a --- /dev/null +++ b/core/src/main/java/org/nzbhydra/NativeHints.java @@ -0,0 +1,69 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra; + +import org.commonmark.renderer.html.HtmlRenderer; +import org.nzbhydra.config.migration.ConfigMigrationStep; +import org.nzbhydra.springnative.ReflectionMarker; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class NativeHints implements RuntimeHintsRegistrar { + + private static final Logger logger = LoggerFactory.getLogger(NativeHints.class); + + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + logger.info("Registering native hints"); + + hints.resources().registerResourceBundle("joptsimple.ExceptionMessages"); + + + final Set> classes = getClassesToRegister(); + classes.add(HashSet.class); + classes.add(ArrayList.class); + classes.add(HtmlRenderer.class); + for (Class clazz : classes) { + hints.reflection().registerType(clazz, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + for (Method method : clazz.getDeclaredMethods()) { + logger.info("Registering " + method + " for reflection"); + hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + } + } + + } + + private static Set> getClassesToRegister() { + final Reflections reflections = new Reflections("org.nzbhydra", Scanners.TypesAnnotated, Scanners.SubTypes); + final Set> classes = reflections.getTypesAnnotatedWith(ReflectionMarker.class); + classes.addAll(reflections.getSubTypesOf(ConfigMigrationStep.class)); + return classes; + } + +} diff --git a/core/src/main/java/org/nzbhydra/NzbHydra.java b/core/src/main/java/org/nzbhydra/NzbHydra.java index a8f4cc923..69919a4d4 100644 --- a/core/src/main/java/org/nzbhydra/NzbHydra.java +++ b/core/src/main/java/org/nzbhydra/NzbHydra.java @@ -2,19 +2,21 @@ package org.nzbhydra; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.base.Strings; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; +import org.apache.commons.lang3.tuple.Pair; import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.ConfigReaderWriter; import org.nzbhydra.config.migration.ConfigMigration; -import org.nzbhydra.database.DatabaseRecreation; import org.nzbhydra.debuginfos.DebugInfosProvider; import org.nzbhydra.genericstorage.GenericStorage; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.misc.BrowserOpener; -import org.nzbhydra.update.UpdateManager; import org.nzbhydra.web.UrlCalculator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,31 +35,26 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.web.bind.annotation.RestController; import org.yaml.snakeyaml.error.YAMLException; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.swing.*; -import java.awt.GraphicsEnvironment; -import java.awt.HeadlessException; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.net.URI; +import java.nio.file.Files; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +@ImportRuntimeHints(NativeHints.class) @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { - AopAutoConfiguration.class, org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration.class}) + AopAutoConfiguration.class, org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration.class}) @ComponentScan @RestController @EnableCaching @@ -73,7 +70,7 @@ public class NzbHydra { private static String dataFolder = null; private static boolean wasRestarted = false; private static boolean anySettingsOverwritten = false; - private static ConfigReaderWriter configReaderWriter = new ConfigReaderWriter(); + private static final ConfigReaderWriter CONFIG_READER_WRITER = new ConfigReaderWriter(); @Autowired private ConfigProvider configProvider; @@ -85,12 +82,29 @@ public class NzbHydra { private GenericStorage genericStorage; @Autowired private ApplicationEventPublisher applicationEventPublisher; + @Autowired + private BaseConfigHandler baseConfigHandler; + + @Autowired + private DebugInfosProvider debugInfosProvider; + public static void main(String[] args) throws Exception { - LoggerFactory.getILoggerFactory(); + if (isNativeBuild()) { + logger.warn("Running for native build"); + String dataFolder = "./data"; + NzbHydra.setDataFolder(dataFolder); + System.setProperty("nzbhydra.dataFolder", dataFolder); + System.setProperty("spring.datasource.url", "jdbc:h2:mem:testdb;NON_KEYWORDS=YEAR,DATA,KEY"); - checkJavaVersion(); + setApplicationPropertiesFromConfig(); + + SpringApplication hydraApplication = new SpringApplication(NzbHydra.class); + applicationContext = hydraApplication.run(args); + logger.info("Native application returned"); + return; + } OptionParser parser = new OptionParser(); parser.accepts("datafolder", "Define path to main data folder. Must start with ./ for relative paths").withRequiredArg().defaultsTo("./data"); @@ -108,46 +122,22 @@ public class NzbHydra { logger.error("Invalid startup options detected: {}", e.getMessage()); System.exit(1); } - if (System.getProperty("fromWrapper") == null && Arrays.stream(args).noneMatch(x -> x.equals("directstart"))) { - logger.info("NZBHydra 2 must be started using the wrapper for restart and updates to work. If for some reason you need to start it from the JAR directly provide the command line argument \"directstart\""); - } else if (options.has("help")) { + + setDataFolder(options); + + if (options.has("help")) { parser.printHelpOn(System.out); } else if (options.has("version")) { - String version = new UpdateManager().getAllVersionChangesUpToCurrentVersion().get(0).getVersion(); - logger.info("NZBHydra 2 version: " + version); + System.out.println(DebugInfosProvider.getVersionAndBuildTimestamp().getLeft()); + } else if (System.getProperty("fromWrapper") == null && Arrays.stream(args).noneMatch(x -> x.equals("directstart"))) { + logger.info("NZBHydra 2 must be started using the wrapper for restart and updates to work. If for some reason you need to start it from the JAR directly provide the command line argument \"directstart\""); } else { startup(args, options); } } - private static void checkJavaVersion() { - final String javaVersionString; - int javaMajor = 0; - try { - javaVersionString = System.getProperty("java.version"); - final Matcher matcher = Pattern.compile("(?\\d+)(\\.(?\\d+)\\.(?\\d)+[\\-_\\w]*)?.*").matcher(javaVersionString); - if (!matcher.find()) { - logger.error("Unable to determine JAVA version from {}", javaVersionString); - return; - } - - javaMajor = Integer.parseInt(matcher.group("major")); - int javaMinor = Integer.parseInt(matcher.group("minor")); - int javaVersion = 0; - if ((javaMajor == 1 && javaMinor == 8)) { - return; - } - } catch (Exception e) { - logger.error("Unable to determine java version", e); - return; - } - if (javaMajor > 17) { - throw new RuntimeException("Deteted Java version " + javaVersionString + ". Please use Java 8, 11, 15 or 17. Java 18 and above are not supported"); - } - } - - protected static void startup(String[] args, OptionSet options) throws Exception { + private static void setDataFolder(OptionSet options) throws IOException { if (options.has("datafolder")) { dataFolder = (String) options.valueOf("datafolder"); } else { @@ -155,6 +145,10 @@ public class NzbHydra { } File dataFolderFile = new File(dataFolder); dataFolder = dataFolderFile.getCanonicalPath(); + } + + protected static void startup(String[] args, OptionSet options) throws Exception { + File dataFolderFile = new File(dataFolder); //Check if we can write in the data folder. If not we can just quit now if (!dataFolderFile.exists() && !dataFolderFile.mkdirs()) { logger.error("Unable to read or write data folder {}", dataFolder); @@ -185,17 +179,13 @@ public class NzbHydra { SpringApplication hydraApplication = new SpringApplication(NzbHydra.class); NzbHydra.originalArgs = args; wasRestarted = Arrays.asList(args).contains("restarted"); - if (NzbHydra.isOsWindows() && !options.has("quiet") && !options.has("nobrowser")) { - hydraApplication.setHeadless(false); - } else { - hydraApplication.setHeadless(true); - } - - DatabaseRecreation.runDatabaseScript(); - + hydraApplication.setHeadless(true); applicationContext = hydraApplication.run(args); } catch (Exception e) { - handleException(e); + //Is thrown by SpringApplicationAotProcessor + if (!(e instanceof SpringApplication.AbandonedRunException)) { + handleException(e); + } } } @@ -204,7 +194,7 @@ public class NzbHydra { * Sets all properties referenced in application.properties so that they can be resolved */ private static void setApplicationPropertiesFromConfig() throws IOException { - BaseConfig baseConfig = configReaderWriter.loadSavedConfig(); + BaseConfig baseConfig = CONFIG_READER_WRITER.loadSavedConfig(); setApplicationProperty("main.host", "MAIN_HOST", baseConfig.getMain().getHost()); setApplicationProperty("main.port", "MAIN_PORT", String.valueOf(baseConfig.getMain().getPort())); setApplicationProperty("main.urlBase", "MAIN_URL_BASE", baseConfig.getMain().getUrlBase().orElse("/")); @@ -230,9 +220,9 @@ public class NzbHydra { File systemErrLogFile = new File(NzbHydra.getDataFolder(), "logs/system.err.log"); File systemOutLogFile = new File(NzbHydra.getDataFolder(), "logs/system.out.log"); logger.info("Enabling SSL debugging. Will write to {}", systemErrLogFile); - System.setErr(new PrintStream(new FileOutputStream(systemErrLogFile))); + System.setErr(new PrintStream(Files.newOutputStream(systemErrLogFile.toPath()))); logger.info("Redirecting console output to system.out.log. You will not see any more log output in the console until you disable the HTTPS marker and restart NZBHydra"); - System.setOut(new PrintStream(new FileOutputStream(systemOutLogFile))); + System.setOut(new PrintStream(Files.newOutputStream(systemOutLogFile.toPath()))); } } } @@ -245,11 +235,14 @@ public class NzbHydra { } private static void initializeAndValidateAndMigrateYamlFile(File yamlFile) throws IOException { - configReaderWriter.initializeIfNeeded(yamlFile); - configReaderWriter.validateExistingConfig(); - Map map = configReaderWriter.loadSavedConfigAsMap(); + if (NzbHydra.isNativeBuild()) { + return; + } + CONFIG_READER_WRITER.initializeIfNeeded(yamlFile); + CONFIG_READER_WRITER.validateExistingConfig(); + Map map = CONFIG_READER_WRITER.loadSavedConfigAsMap(); Map migrated = new ConfigMigration().migrate(map); - configReaderWriter.save(migrated, yamlFile); + CONFIG_READER_WRITER.save(migrated, yamlFile); } private static void handleException(Exception e) throws Exception { @@ -272,39 +265,19 @@ public class NzbHydra { msg = "An unexpected error occurred during startup:\n" + e; logger.error("An unexpected error occurred during startup", e); } - try { - if (!GraphicsEnvironment.isHeadless() && isOsWindows()) { - final String htmlMessage = "" + msg.replace("\n", "
") + ""; - JOptionPane.showMessageDialog(null, htmlMessage, "NZBHydra 2 error", JOptionPane.ERROR_MESSAGE); - } - } catch (HeadlessException e1) { - logger.warn("Unable to show exception in message dialog: {}", e1.getMessage()); - } + logger.error("FATAL: " + msg, e); + //Rethrow so that spring exception handlers can handle this throw e; } - - @PostConstruct - private void addTrayIconIfApplicable() { - boolean isOsWindows = isOsWindows(); - if (isOsWindows) { - logger.info("Adding windows system tray icon"); - try { - new WindowsTrayIcon(); - } catch (Throwable e) { - logger.error("Can't add a windows tray icon because running headless"); - } - } - } - public static boolean isOsWindows() { String osName = System.getProperty("os.name"); return osName.toLowerCase().contains("windows"); } @PostConstruct - private void warnIfSettingsOverwritten() { + public void warnIfSettingsOverwritten() { if (anySettingsOverwritten) { logger.warn("Overwritten settings will be displayed with their original value in the config section of the GUI"); } @@ -338,21 +311,26 @@ public class NzbHydra { //I don't know why I have to do this but otherwise genericStorage is always empty configProvider.getBaseConfig().setGenericStorage(new ConfigReaderWriter().loadSavedConfig().getGenericStorage()); - if (!genericStorage.get("FirstStart", LocalDateTime.class).isPresent()) { + final Pair versionAndBuildTimestamp = DebugInfosProvider.getVersionAndBuildTimestamp(); + logger.info("Version: {}", versionAndBuildTimestamp.getLeft()); + logger.info("Build timestamp: {}", versionAndBuildTimestamp.getRight()); + if (genericStorage.get("FirstStart", LocalDateTime.class).isEmpty()) { logger.info("First start of NZBHydra detected"); genericStorage.save("FirstStart", LocalDateTime.now()); - configProvider.getBaseConfig().save(false); + baseConfigHandler.save(false); } + if (DebugInfosProvider.isRunInDocker()) { logger.info("You seem to be running NZBHydra 2 in docker. You can access Hydra using your local address and the IP you provided"); - } else if (configProvider.getBaseConfig().getMain().isStartupBrowser() && !"true".equals(System.getProperty(BROWSER_DISABLED))) { - if (wasRestarted) { - logger.info("Not opening browser after restart"); - return; - } - browserOpener.openBrowser(); } else { + if (configProvider.getBaseConfig().getMain().isStartupBrowser() && !"true".equals(System.getProperty(BROWSER_DISABLED))) { + if (wasRestarted) { + logger.info("Not opening browser after restart"); + return; + } + browserOpener.openBrowser(); + } URI uri = urlCalculator.getLocalBaseUriBuilder().build().toUri(); logger.info("You can access NZBHydra 2 in your browser via {}", uri); } @@ -363,16 +341,6 @@ public class NzbHydra { @PreDestroy public void destroy() { - boolean isOsWindows = isOsWindows(); - if (isOsWindows) { - logger.debug("Initiating removal of windows tray icon (if it exists)"); - try { - WindowsTrayIcon.remove(); - } catch (Throwable e) { - //An exception might be thrown while shutting down, ignore this - } - } - applicationEventPublisher.publishEvent(new ShutdownEvent()); logger.info("Shutting down and using up to {}ms to compact database", configProvider.getBaseConfig().getMain().getDatabaseCompactTime()); } @@ -382,5 +350,11 @@ public class NzbHydra { return new CaffeineCacheManager("infos", "titles", "updates", "dev"); } + static void setDataFolder(String dataFolder) { + NzbHydra.dataFolder = dataFolder; + } + public static boolean isNativeBuild() { + return System.getenv("HYDRA_NATIVE_BUILD") != null; + } } diff --git a/core/src/main/java/org/nzbhydra/NzbHydraNativeEntrypoint.java b/core/src/main/java/org/nzbhydra/NzbHydraNativeEntrypoint.java new file mode 100644 index 000000000..37ef2d4e3 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/NzbHydraNativeEntrypoint.java @@ -0,0 +1,48 @@ +/* + * (C) Copyright 2022 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra; + +import jakarta.annotation.PostConstruct; +import org.springframework.boot.SpringApplicationAotProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.aot.AbstractAotProcessor; +import org.springframework.javapoet.ClassName; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.nio.file.Paths; +import java.util.Arrays; + +@Configuration +public class NzbHydraNativeEntrypoint { + + public static void main(String[] args) throws Exception { + int requiredArgs = 6; + Assert.isTrue(args.length >= requiredArgs, () -> "Usage: " + SpringApplicationAotProcessor.class.getName() + + " "); + Class application = Class.forName(args[0]); + AbstractAotProcessor.Settings settings = AbstractAotProcessor.Settings.builder().sourceOutput(Paths.get(args[1])).resourceOutput(Paths.get(args[2])) + .classOutput(Paths.get(args[3])).groupId((StringUtils.hasText(args[4])) ? args[4] : "unspecified") + .artifactId(args[5]).build(); + String[] applicationArgs = (args.length > requiredArgs) ? Arrays.copyOfRange(args, requiredArgs, args.length) + : new String[0]; + final SpringApplicationAotProcessor processor = new SpringApplicationAotProcessor(application, settings, applicationArgs); + final ClassName process = processor.process(); + System.exit(0); + } + +} diff --git a/core/src/main/java/org/nzbhydra/WindowsTrayIcon.java b/core/src/main/java/org/nzbhydra/WindowsTrayIcon.java deleted file mode 100644 index 799a7c53f..000000000 --- a/core/src/main/java/org/nzbhydra/WindowsTrayIcon.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.nzbhydra; - -import org.nzbhydra.misc.BrowserOpener; -import org.nzbhydra.systemcontrol.SystemControl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ConfigurableApplicationContext; - -import javax.swing.*; -import java.awt.AWTException; -import java.awt.MenuItem; -import java.awt.PopupMenu; -import java.awt.SystemTray; -import java.awt.TrayIcon; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; - -//Mostly taken from https://stackoverflow.com/a/44452260 -public class WindowsTrayIcon extends TrayIcon { - - private static final Logger logger = LoggerFactory.getLogger(WindowsTrayIcon.class); - - private static final String IMAGE_PATH = "/nzbhydra.png"; - private static final String TOOLTIP = "NZBHydra 2"; - private PopupMenu popup; - private static SystemTray tray; - private static WindowsTrayIcon instance; - - public WindowsTrayIcon() { - super(new ImageIcon(WindowsTrayIcon.class.getResource(IMAGE_PATH), TOOLTIP).getImage(), TOOLTIP); - try { - popup = new PopupMenu(); - tray = SystemTray.getSystemTray(); - instance = this; - setup(); - } catch (AWTException e) { - logger.error("Unable to create tray icon", e); - } - } - - private void setup() throws AWTException { - MenuItem openBrowserItem = new MenuItem("Open web UI"); - popup.add(openBrowserItem); - openBrowserItem.addActionListener(e -> { - openBrowser(); - }); - - MenuItem restartItem = new MenuItem("Restart"); - popup.add(restartItem); - restartItem.addActionListener(e -> { - ((ConfigurableApplicationContext) NzbHydra.getApplicationContext()).close(); - remove(); - System.exit(SystemControl.RESTART_RETURN_CODE); - }); - - MenuItem shutdownItem = new MenuItem("Shutdown"); - popup.add(shutdownItem); - shutdownItem.addActionListener(e -> { - try { - ((ConfigurableApplicationContext) NzbHydra.getApplicationContext()).close(); - } catch (Exception e1) { - logger.error("Error while closing application context, will shut down hard"); - } finally { - remove(); - } - System.exit(SystemControl.SHUTDOWN_RETURN_CODE); - }); - - - setPopupMenu(popup); - tray.add(this); - instance.addMouseListener(new MouseListener() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - openBrowser(); - } - } - - @Override - public void mousePressed(MouseEvent e) { - - } - - @Override - public void mouseReleased(MouseEvent e) { - - } - - @Override - public void mouseEntered(MouseEvent e) { - - } - - @Override - public void mouseExited(MouseEvent e) { - - } - }); - } - - private void openBrowser() { - try { - NzbHydra.getApplicationContext().getAutowireCapableBeanFactory().createBean(BrowserOpener.class).openBrowser(); - } catch (NullPointerException | IllegalStateException | BeansException e1) { - logger.error("Unable to open browser. Process may not have started completely"); - } - } - - public static void remove() { - try { - if (tray != null && instance != null) { - logger.info("Removing tray icon"); - tray.remove(instance); - tray = null; - } - } catch (Throwable e) { - logger.error("Unable to remove tray icon", e); - } - } - -} diff --git a/core/src/main/java/org/nzbhydra/api/CapsGenerator.java b/core/src/main/java/org/nzbhydra/api/CapsGenerator.java index ccb62650e..72163f0f3 100644 --- a/core/src/main/java/org/nzbhydra/api/CapsGenerator.java +++ b/core/src/main/java/org/nzbhydra/api/CapsGenerator.java @@ -18,10 +18,12 @@ package org.nzbhydra.api; import com.google.common.collect.Sets; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.SearchSourceRestriction; import org.nzbhydra.config.category.Category; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; +import org.nzbhydra.config.mediainfo.MediaIdType; import org.nzbhydra.mapping.newznab.ActionAttribute; import org.nzbhydra.mapping.newznab.NewznabResponse; import org.nzbhydra.mapping.newznab.OutputType; @@ -47,8 +49,6 @@ import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlSearch; import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlSearching; import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlServer; import org.nzbhydra.mediainfo.InfoProvider; -import org.nzbhydra.mediainfo.MediaIdType; -import org.nzbhydra.searching.searchrequests.SearchRequest; import org.nzbhydra.update.UpdateManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -66,7 +66,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; @Component public class CapsGenerator { @@ -174,7 +173,7 @@ public class CapsGenerator { if (x.getState() != IndexerConfig.State.ENABLED && x.getState() != IndexerConfig.State.DISABLED_SYSTEM_TEMPORARY) { return false; } - if (!x.getEnabledForSearchSource().meets(SearchRequest.SearchSource.API)) { + if (!SearchSource.API.meets(x.getEnabledForSearchSource())) { //Indexer will not be picked for API searches return false; } @@ -217,9 +216,9 @@ public class CapsGenerator { } for (Category category : configProvider.getBaseConfig().getCategoriesConfig().getCategories()) { List subCategories = category.getNewznabCategories().stream().flatMap(Collection::stream).filter(x -> x % 1000 != 0) - //Lower numbers first so that predefined category numbers take precedence over custom ones - .sorted(Comparator.naturalOrder()) - .collect(Collectors.toList()); + //Lower numbers first so that predefined category numbers take precedence over custom ones + .sorted(Comparator.naturalOrder()) + .toList(); //Use lowest category first for (Integer subCategory : subCategories) { if (alreadyAdded.contains(category.getName())) { diff --git a/core/src/main/java/org/nzbhydra/api/CategoryConverter.java b/core/src/main/java/org/nzbhydra/api/CategoryConverter.java index a4ffc7178..caf6fc22f 100644 --- a/core/src/main/java/org/nzbhydra/api/CategoryConverter.java +++ b/core/src/main/java/org/nzbhydra/api/CategoryConverter.java @@ -1,13 +1,12 @@ package org.nzbhydra.api; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import org.nzbhydra.config.category.Category; import org.nzbhydra.searching.CategoryProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; - @Converter @Component diff --git a/core/src/main/java/org/nzbhydra/api/ExternalApi.java b/core/src/main/java/org/nzbhydra/api/ExternalApi.java index 7f2461832..97acb75d9 100644 --- a/core/src/main/java/org/nzbhydra/api/ExternalApi.java +++ b/core/src/main/java/org/nzbhydra/api/ExternalApi.java @@ -8,7 +8,11 @@ import lombok.AllArgsConstructor; import lombok.Data; import org.apache.catalina.connector.ClientAbortException; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.category.CategoriesConfig; +import org.nzbhydra.config.downloading.DownloadType; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.downloading.DownloadResult; import org.nzbhydra.downloading.FileHandler; import org.nzbhydra.downloading.InvalidSearchResultIdException; @@ -19,16 +23,13 @@ import org.nzbhydra.mapping.newznab.NewznabResponse; import org.nzbhydra.mapping.newznab.OutputType; import org.nzbhydra.mapping.newznab.xml.NewznabXmlError; import org.nzbhydra.mediainfo.Imdb; -import org.nzbhydra.mediainfo.MediaIdType; import org.nzbhydra.searching.CategoryProvider; -import org.nzbhydra.searching.CustomQueryAndTitleMapping; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; import org.nzbhydra.searching.SearchResult; import org.nzbhydra.searching.Searcher; -import org.nzbhydra.searching.dtoseventsenums.DownloadType; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.nzbhydra.searching.searchrequests.SearchRequestFactory; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.web.SessionStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -90,7 +91,7 @@ public class ExternalApi { @Autowired private MockSearch mockSearch; @Autowired - private CustomQueryAndTitleMapping customQueryAndTitleMapping; + private CustomQueryAndTitleMappingHandler customQueryAndTitleMappingHandler; protected Clock clock = Clock.systemUTC(); private final Random random = new Random(); @@ -336,13 +337,14 @@ public class ExternalApi { searchRequest.getInternalData().setIncludePasswords(true); } searchRequest = searchRequestFactory.extendWithSavedIdentifiers(searchRequest); - searchRequest = customQueryAndTitleMapping.mapSearchRequest(searchRequest); + searchRequest = customQueryAndTitleMappingHandler.mapSearchRequest(searchRequest); return searchRequest; } @Data +@ReflectionMarker @AllArgsConstructor private static class CacheEntryValue { private final NewznabParameters params; diff --git a/core/src/main/java/org/nzbhydra/api/NewznabJsonTransformer.java b/core/src/main/java/org/nzbhydra/api/NewznabJsonTransformer.java index 0f254737c..d14fed7a6 100644 --- a/core/src/main/java/org/nzbhydra/api/NewznabJsonTransformer.java +++ b/core/src/main/java/org/nzbhydra/api/NewznabJsonTransformer.java @@ -17,6 +17,7 @@ package org.nzbhydra.api; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.downloading.FileHandler; import org.nzbhydra.mapping.newznab.NewznabResponse; import org.nzbhydra.mapping.newznab.json.NewznabJsonChannel; @@ -29,7 +30,6 @@ import org.nzbhydra.mapping.newznab.json.NewznabJsonItemAttributes; import org.nzbhydra.mapping.newznab.json.NewznabJsonResponseAttributes; import org.nzbhydra.mapping.newznab.json.NewznabJsonRoot; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/core/src/main/java/org/nzbhydra/api/NewznabXmlTransformer.java b/core/src/main/java/org/nzbhydra/api/NewznabXmlTransformer.java index 1603c280f..33a8a7d0a 100644 --- a/core/src/main/java/org/nzbhydra/api/NewznabXmlTransformer.java +++ b/core/src/main/java/org/nzbhydra/api/NewznabXmlTransformer.java @@ -17,6 +17,7 @@ package org.nzbhydra.api; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.downloading.FileHandler; import org.nzbhydra.mapping.newznab.NewznabResponse; import org.nzbhydra.mapping.newznab.xml.NewznabAttribute; @@ -27,7 +28,6 @@ import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; import org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/core/src/main/java/org/nzbhydra/api/stats/ApiStatsRequest.java b/core/src/main/java/org/nzbhydra/api/stats/ApiStatsRequest.java index ba7acac3c..462bdf53d 100644 --- a/core/src/main/java/org/nzbhydra/api/stats/ApiStatsRequest.java +++ b/core/src/main/java/org/nzbhydra/api/stats/ApiStatsRequest.java @@ -18,8 +18,10 @@ package org.nzbhydra.api.stats; import lombok.Data; import org.nzbhydra.historystats.stats.StatsRequest; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker public class ApiStatsRequest { protected String apikey; diff --git a/core/src/main/java/org/nzbhydra/auth/AsyncSupportFilter.java b/core/src/main/java/org/nzbhydra/auth/AsyncSupportFilter.java index 0f9e103b0..1f8e03363 100644 --- a/core/src/main/java/org/nzbhydra/auth/AsyncSupportFilter.java +++ b/core/src/main/java/org/nzbhydra/auth/AsyncSupportFilter.java @@ -17,14 +17,14 @@ package org.nzbhydra.auth; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.catalina.Globals; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component diff --git a/core/src/main/java/org/nzbhydra/auth/AuthAndAccessEventHandler.java b/core/src/main/java/org/nzbhydra/auth/AuthAndAccessEventHandler.java index eba7eaaeb..8fc28b3f5 100644 --- a/core/src/main/java/org/nzbhydra/auth/AuthAndAccessEventHandler.java +++ b/core/src/main/java/org/nzbhydra/auth/AuthAndAccessEventHandler.java @@ -1,5 +1,8 @@ package org.nzbhydra.auth; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.nzbhydra.notifications.AuthFailureNotificationEvent; import org.nzbhydra.web.SessionStorage; import org.slf4j.Logger; @@ -17,9 +20,6 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @@ -69,7 +69,7 @@ public class AuthAndAccessEventHandler extends AccessDeniedHandlerImpl { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { - logger.warn("Access denied to IP {}: {}", SessionStorage.IP.get(), accessDeniedException.getMessage()); + logger.warn("Access denied to IP {}: {}. Request path: {}. Parameters: {}", SessionStorage.IP.get(), accessDeniedException.getMessage(), request.getContextPath(), request.getParameterMap()); attemptService.accessFailed(SessionStorage.IP.get()); super.handle(request, response, accessDeniedException); } diff --git a/core/src/main/java/org/nzbhydra/auth/AuthWeb.java b/core/src/main/java/org/nzbhydra/auth/AuthWeb.java index 11b21bac9..49d4be5ed 100644 --- a/core/src/main/java/org/nzbhydra/auth/AuthWeb.java +++ b/core/src/main/java/org/nzbhydra/auth/AuthWeb.java @@ -1,5 +1,6 @@ package org.nzbhydra.auth; +import jakarta.servlet.http.HttpSession; import org.nzbhydra.web.BootstrappedDataTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -10,7 +11,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpSession; import java.security.Principal; @RestController diff --git a/core/src/main/java/org/nzbhydra/auth/ForwardedForRecognizingFilter.java b/core/src/main/java/org/nzbhydra/auth/ForwardedForRecognizingFilter.java index d319e16ea..515585652 100644 --- a/core/src/main/java/org/nzbhydra/auth/ForwardedForRecognizingFilter.java +++ b/core/src/main/java/org/nzbhydra/auth/ForwardedForRecognizingFilter.java @@ -16,15 +16,15 @@ package org.nzbhydra.auth; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.nzbhydra.web.SessionStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.filter.OncePerRequestFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class ForwardedForRecognizingFilter extends OncePerRequestFilter { diff --git a/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java b/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java index 4bffdca55..4843a49db 100644 --- a/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java +++ b/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java @@ -17,24 +17,27 @@ package org.nzbhydra.auth; import com.google.common.net.InetAddresses; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.nzbhydra.config.auth.AuthConfig; import org.nzbhydra.web.SessionStorage; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Objects; public class HeaderAuthenticationFilter extends BasicAuthenticationFilter { @@ -43,14 +46,36 @@ public class HeaderAuthenticationFilter extends BasicAuthenticationFilter { private final HydraUserDetailsManager userDetailsManager; private AuthConfig authConfig; + private final String internalApiKey; + public HeaderAuthenticationFilter(AuthenticationManager authenticationManager, HydraUserDetailsManager userDetailsManager, AuthConfig authConfig) { super(authenticationManager); this.userDetailsManager = userDetailsManager; this.authConfig = authConfig; + //Must be provided by wrapper + internalApiKey = System.getProperty("internalApiKey"); + if (internalApiKey != null) { + logger.info("Using internal API key"); + } } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + final String sentInternalApiKey = request.getParameterValues("internalApiKey") == null ? null : request.getParameterValues("internalApiKey")[0]; + if (sentInternalApiKey != null) { + if (Objects.equals(sentInternalApiKey, internalApiKey)) { + + final AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("key", "internalApi", AuthorityUtils.createAuthorityList("ROLE_ADMIN")); + token.setDetails(new HydraWebAuthenticationDetails(request)); + SecurityContextHolder.getContext().setAuthentication(token); + onSuccessfulAuthentication(request, response, token); + logger.debug("Authorized access to {} via internal API key", request.getRequestURI()); + chain.doFilter(request, response); + return; + } else { + logger.warn("Invalid internal API key provided"); + } + } if (authConfig.getAuthHeader() == null || authConfig.getAuthHeaderIpRanges().isEmpty()) { chain.doFilter(request, response); return; diff --git a/core/src/main/java/org/nzbhydra/auth/HydraAnonymousAuthenticationFilter.java b/core/src/main/java/org/nzbhydra/auth/HydraAnonymousAuthenticationFilter.java index a5cd387bd..f954a7ce6 100644 --- a/core/src/main/java/org/nzbhydra/auth/HydraAnonymousAuthenticationFilter.java +++ b/core/src/main/java/org/nzbhydra/auth/HydraAnonymousAuthenticationFilter.java @@ -1,7 +1,12 @@ package org.nzbhydra.auth; -import org.nzbhydra.config.BaseConfig; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; import org.nzbhydra.config.ConfigChangedEvent; +import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.auth.AuthConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,11 +23,6 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS import org.springframework.stereotype.Component; import org.springframework.util.Assert; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -43,9 +43,9 @@ public class HydraAnonymousAuthenticationFilter extends AnonymousAuthenticationF //Disabled by default because just by existing it will be used for static resource accesses where spring security is disabled private boolean enabled = false; - public HydraAnonymousAuthenticationFilter(@Autowired BaseConfig baseConfig) { + public HydraAnonymousAuthenticationFilter(@Autowired ConfigProvider configProvider) { super("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - updateAuthorities(baseConfig.getAuth()); + updateAuthorities(configProvider.getBaseConfig().getAuth()); } public void enable() { diff --git a/core/src/main/java/org/nzbhydra/auth/HydraEmbeddedServletContainer.java b/core/src/main/java/org/nzbhydra/auth/HydraEmbeddedServletContainer.java index 513e45f8e..5754817fc 100644 --- a/core/src/main/java/org/nzbhydra/auth/HydraEmbeddedServletContainer.java +++ b/core/src/main/java/org/nzbhydra/auth/HydraEmbeddedServletContainer.java @@ -16,6 +16,7 @@ package org.nzbhydra.auth; +import jakarta.servlet.ServletException; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; @@ -27,7 +28,6 @@ import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.stereotype.Component; -import javax.servlet.ServletException; import java.io.IOException; /** @@ -41,10 +41,9 @@ public class HydraEmbeddedServletContainer implements WebServerFactoryCustomizer @Override public void customize(ConfigurableServletWebServerFactory factory) { - if (!(factory instanceof TomcatServletWebServerFactory)) { + if (!(factory instanceof TomcatServletWebServerFactory containerFactory)) { return; //Is the case in tests } - TomcatServletWebServerFactory containerFactory = (TomcatServletWebServerFactory) factory; containerFactory.addContextValves(new ValveBase() { @Override public void invoke(Request request, Response response) throws IOException, ServletException { diff --git a/core/src/main/java/org/nzbhydra/auth/HydraGlobalMethodSecurityConfiguration.java b/core/src/main/java/org/nzbhydra/auth/HydraGlobalMethodSecurityConfiguration.java index d89c0aa71..fc0e13a0e 100644 --- a/core/src/main/java/org/nzbhydra/auth/HydraGlobalMethodSecurityConfiguration.java +++ b/core/src/main/java/org/nzbhydra/auth/HydraGlobalMethodSecurityConfiguration.java @@ -16,7 +16,7 @@ package org.nzbhydra.auth; -import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.auth.AuthType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,14 +40,14 @@ public class HydraGlobalMethodSecurityConfiguration extends GlobalMethodSecurity private static final Logger hydraLogger = LoggerFactory.getLogger(HydraGlobalMethodSecurityConfiguration.class); @Autowired - private BaseConfig baseConfig; + private ConfigProvider configProvider; @Bean public MethodSecurityMetadataSource methodSecurityMetadataSource() { List sources = new ArrayList<>(); - if (baseConfig.getAuth().getAuthType() != AuthType.NONE) { - hydraLogger.info("Enabling auth type " + baseConfig.getAuth().getAuthType()); + if (configProvider.getBaseConfig().getAuth().getAuthType() != AuthType.NONE) { + hydraLogger.info("Enabling auth type " + configProvider.getBaseConfig().getAuth().getAuthType()); sources.add(new SecuredAnnotationSecurityMetadataSource()); } diff --git a/core/src/main/java/org/nzbhydra/auth/HydraWebAuthenticationDetails.java b/core/src/main/java/org/nzbhydra/auth/HydraWebAuthenticationDetails.java index b24bb7c3d..a1cb1a290 100644 --- a/core/src/main/java/org/nzbhydra/auth/HydraWebAuthenticationDetails.java +++ b/core/src/main/java/org/nzbhydra/auth/HydraWebAuthenticationDetails.java @@ -1,10 +1,9 @@ package org.nzbhydra.auth; import com.google.common.base.Objects; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.web.authentication.WebAuthenticationDetails; -import javax.servlet.http.HttpServletRequest; - public class HydraWebAuthenticationDetails extends WebAuthenticationDetails { private final String filteredIp; @@ -39,10 +38,9 @@ public class HydraWebAuthenticationDetails extends WebAuthenticationDetails { if (this == o) { return true; } - if (!(o instanceof HydraWebAuthenticationDetails)) { + if (!(o instanceof HydraWebAuthenticationDetails that)) { return false; } - HydraWebAuthenticationDetails that = (HydraWebAuthenticationDetails) o; return Objects.equal(filteredIp, that.filteredIp); } diff --git a/core/src/main/java/org/nzbhydra/auth/LoginAndAccessAttemptService.java b/core/src/main/java/org/nzbhydra/auth/LoginAndAccessAttemptService.java index 0f4d24eab..b568d1f5a 100644 --- a/core/src/main/java/org/nzbhydra/auth/LoginAndAccessAttemptService.java +++ b/core/src/main/java/org/nzbhydra/auth/LoginAndAccessAttemptService.java @@ -19,7 +19,7 @@ public class LoginAndAccessAttemptService { private final LoadingCache attemptsCache; public LoginAndAccessAttemptService() { - attemptsCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader() { + attemptsCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<>() { @Override public Integer load(String key) throws Exception { return 0; diff --git a/core/src/main/java/org/nzbhydra/auth/PersistentLoginsEntity.java b/core/src/main/java/org/nzbhydra/auth/PersistentLoginsEntity.java index 8c5d1dab8..0188bdd2a 100644 --- a/core/src/main/java/org/nzbhydra/auth/PersistentLoginsEntity.java +++ b/core/src/main/java/org/nzbhydra/auth/PersistentLoginsEntity.java @@ -1,12 +1,13 @@ package org.nzbhydra.auth; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.validation.constraints.NotNull; import java.sql.Timestamp; /** @@ -16,7 +17,8 @@ import java.sql.Timestamp; @Entity @Table(name = "persistent_logins") @Data -public class PersistentLoginsEntity { +@ReflectionMarker +public final class PersistentLoginsEntity { @Id @NotNull diff --git a/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java b/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java index 36a38da76..6263ca419 100644 --- a/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java +++ b/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java @@ -1,5 +1,6 @@ package org.nzbhydra.auth; +import jakarta.servlet.http.HttpServletRequest; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigChangedEvent; import org.nzbhydra.config.ConfigProvider; @@ -12,26 +13,25 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.firewall.DefaultHttpFirewall; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.filter.ForwardedHeaderFilter; -import javax.servlet.http.HttpServletRequest; - @Configuration @Order @EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { +public class SecurityConfig { private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); private static final int SECONDS_PER_DAY = 60 * 60 * 24; @@ -45,95 +45,122 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthAndAccessEventHandler authAndAccessEventHandler; @Autowired + private UserDetailsService userDetailsService; + @Autowired private AsyncSupportFilter asyncSupportFilter; private HeaderAuthenticationFilter headerAuthenticationFilter; - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { BaseConfig baseConfig = configProvider.getBaseConfig(); - if (configProvider.getBaseConfig().getMain().isUseCsrf()) { + boolean useCsrf = Boolean.parseBoolean(System.getProperty("main.useCsrf")); + if (configProvider.getBaseConfig().getMain().isUseCsrf() && useCsrf) { CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); csrfTokenRepository.setCookieName("HYDRA-XSRF-TOKEN"); - http.csrf().csrfTokenRepository(csrfTokenRepository); + //https://docs.spring.io/spring-security/reference/5.8/migration/servlet/exploits.html#_i_am_using_angularjs_or_another_javascript_framework + CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); + requestHandler.setCsrfRequestAttributeName(null); + http.csrf() + .csrfTokenRepository(csrfTokenRepository) + .csrfTokenRequestHandler(requestHandler); } else { + logger.info("CSRF is disabled"); http.csrf().disable(); } - http.headers().httpStrictTransportSecurity().disable(); - http.headers().frameOptions().disable(); + http.headers() + .httpStrictTransportSecurity().disable() + .frameOptions().disable(); if (baseConfig.getAuth().getAuthType() == AuthType.BASIC) { http = http - .authorizeRequests() - .antMatchers("/internalapi/userinfos").permitAll() - .and() - .httpBasic() - .authenticationDetailsSource(new WebAuthenticationDetailsSource() { - @Override - public WebAuthenticationDetails buildDetails(HttpServletRequest context) { - return new HydraWebAuthenticationDetails(context); - } - }) - .and() - .logout().logoutUrl("/logout").deleteCookies("remember-me") - .and(); + .httpBasic() + .authenticationDetailsSource(new WebAuthenticationDetailsSource() { + @Override + public WebAuthenticationDetails buildDetails(HttpServletRequest context) { + return new HydraWebAuthenticationDetails(context); + } + }) + .and(); } else if (baseConfig.getAuth().getAuthType() == AuthType.FORM) { http = http - .authorizeRequests() - .antMatchers("/internalapi/userinfos").permitAll() - .and() - .formLogin().loginPage("/login").loginProcessingUrl("/login").defaultSuccessUrl("/").permitAll() - .authenticationDetailsSource(new WebAuthenticationDetailsSource() { - @Override - public WebAuthenticationDetails buildDetails(HttpServletRequest context) { - return new HydraWebAuthenticationDetails(context); - } - }) - .and() - .logout().permitAll().logoutUrl("/logout").logoutSuccessUrl("/loggedout").logoutSuccessUrl("/").deleteCookies("remember-me") - .and(); + .formLogin() + .loginPage("/login") + .loginProcessingUrl("/login") + .defaultSuccessUrl("/") + .permitAll() + .authenticationDetailsSource(new WebAuthenticationDetailsSource() { + @Override + public WebAuthenticationDetails buildDetails(HttpServletRequest context) { + return new HydraWebAuthenticationDetails(context); + } + }) + .and(); } if (baseConfig.getAuth().isAuthConfigured()) { + http = http + .authorizeHttpRequests() + .requestMatchers("/internalapi/") + .authenticated() + .requestMatchers("/websocket/") + .authenticated() + .requestMatchers("/actuator/**") + .hasRole("ADMIN") + .requestMatchers(new AntPathRequestMatcher("/static/**")) + .permitAll() + .anyRequest() +// .authenticated() //Does not include anonymous + .hasAnyRole("ADMIN", "ANONYMOUS", "USER") + .and() + .logout() + .permitAll() + .logoutUrl("/logout") + .logoutSuccessUrl("/") + .deleteCookies("remember-me") + .invalidateHttpSession(true) + .clearAuthentication(true) + .and() + ; enableAnonymousAccessIfConfigured(http); + if (baseConfig.getAuth().isRememberUsers()) { int rememberMeValidityDays = configProvider.getBaseConfig().getAuth().getRememberMeValidityDays(); if (rememberMeValidityDays == 0) { rememberMeValidityDays = 1000; //Can't be disabled, three years should be enough } http = http - .rememberMe() - .alwaysRemember(true) - .tokenValiditySeconds(rememberMeValidityDays * SECONDS_PER_DAY) - .userDetailsService(userDetailsService()) - .and(); + .rememberMe() + .alwaysRemember(true) + .tokenValiditySeconds(rememberMeValidityDays * SECONDS_PER_DAY) + .userDetailsService(userDetailsService) + .and(); } - http.authorizeRequests() - .antMatchers("/actuator/**") - .hasRole("ADMIN") - .anyRequest().permitAll(); + + headerAuthenticationFilter = new HeaderAuthenticationFilter(authenticationManager, hydraUserDetailsManager, configProvider.getBaseConfig().getAuth()); + http.addFilterAfter(headerAuthenticationFilter, BasicAuthenticationFilter.class); + http.addFilterAfter(asyncSupportFilter, BasicAuthenticationFilter.class); + + } else { + http.authorizeHttpRequests().anyRequest().permitAll(); } - headerAuthenticationFilter = new HeaderAuthenticationFilter(authenticationManager(), hydraUserDetailsManager, configProvider.getBaseConfig().getAuth()); + http.exceptionHandling().accessDeniedHandler(authAndAccessEventHandler); + http.addFilterBefore(new ForwardedForRecognizingFilter(), ChannelProcessingFilter.class); //We need to extract the original IP before it's removed and not retrievable anymore by the ForwardedHeaderFilter http.addFilterAfter(new ForwardedHeaderFilter(), ForwardedForRecognizingFilter.class); - http.addFilterAfter(headerAuthenticationFilter, BasicAuthenticationFilter.class); - http.addFilterAfter(asyncSupportFilter, BasicAuthenticationFilter.class); - - http.exceptionHandling().accessDeniedHandler(authAndAccessEventHandler); + return http.build(); } - @Override - public void configure(WebSecurity web) { - web.ignoring().antMatchers("/static/**"); - } - private void enableAnonymousAccessIfConfigured(HttpSecurity http) { //Create an anonymous auth filter. If any of the areas are not restricted the anonymous user will get its role try { if (!hydraAnonymousAuthenticationFilter.getAuthorities().isEmpty()) { http.anonymous().authenticationFilter(hydraAnonymousAuthenticationFilter); + hydraAnonymousAuthenticationFilter.enable(); + } + } catch (Exception e) { logger.error("Unable to configure anonymous access", e); } @@ -146,21 +173,19 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } } - @Bean(name = BeanIds.AUTHENTICATION_MANAGER) - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - @Bean public DefaultHttpFirewall defaultHttpFirewall() { //Allow duplicate trailing slashes which happen when behind a reverse proxy, e.g. proxy_pass http://127.0.0.1:5076/nzbhydra2/; return new DefaultHttpFirewall(); } - @Override - public void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.userDetailsService(hydraUserDetailsManager); + @Bean + public AuthenticationManager authManager(HttpSecurity http) + throws Exception { + return http.getSharedObject(AuthenticationManagerBuilder.class) + .userDetailsService(hydraUserDetailsManager) + .and() + .build(); } diff --git a/core/src/main/java/org/nzbhydra/backup/BackupAndRestore.java b/core/src/main/java/org/nzbhydra/backup/BackupAndRestore.java index 5f852c778..3fc576a4d 100644 --- a/core/src/main/java/org/nzbhydra/backup/BackupAndRestore.java +++ b/core/src/main/java/org/nzbhydra/backup/BackupAndRestore.java @@ -1,26 +1,28 @@ package org.nzbhydra.backup; import com.google.common.base.Stopwatch; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import com.google.common.base.Throwables; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; import org.apache.commons.io.FileUtils; +import org.h2.message.DbException; import org.nzbhydra.GenericResponse; import org.nzbhydra.NzbHydra; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.ConfigReaderWriter; +import org.nzbhydra.genericstorage.GenericStorage; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.systemcontrol.SystemControl; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.aot.hint.annotation.Reflective; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import javax.annotation.PostConstruct; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -36,7 +38,6 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.spi.FileSystemProvider; -import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -64,6 +65,9 @@ public class BackupAndRestore { private HydraOkHttp3ClientHttpRequestFactory requestFactory; @Autowired private SystemControl systemControl; + + @Autowired + private GenericStorage genericStorage; private final ConfigReaderWriter configReaderWriter = new ConfigReaderWriter(); @PostConstruct @@ -77,7 +81,7 @@ public class BackupAndRestore { } @Transactional - public File backup() throws Exception { + public File backup(boolean triggeredByUsed) throws Exception { Stopwatch stopwatch = Stopwatch.createStarted(); File backupFolder = getBackupFolder(); if (!backupFolder.exists()) { @@ -92,7 +96,10 @@ public class BackupAndRestore { logger.info("Creating backup"); File backupZip = new File(backupFolder, "nzbhydra-" + LocalDateTime.now().format(DATE_PATTERN) + ".zip"); - backupDatabase(backupZip); + backupDatabase(backupZip, triggeredByUsed); + if (!backupZip.exists()) { + throw new RuntimeException("Export to file " + backupZip + " was not executed by database"); + } Map env = new HashMap<>(); env.put("create", "true"); //We use the jar filesystem so we can add files to the existing ZIP @@ -117,8 +124,8 @@ public class BackupAndRestore { logger.info("Deleting old backups if any exist"); Integer backupMaxAgeInWeeks = configProvider.getBaseConfig().getMain().getDeleteBackupsAfterWeeks().get(); File[] zips = backupFolder.listFiles((dir, name) -> name != null && name.startsWith("nzbhydra") && name.endsWith(".zip")); - if (zips != null) { + Map fileToBackupTime = new HashMap<>(); for (File zip : zips) { Matcher matcher = FILE_PATTERN.matcher(zip.getName()); if (!matcher.matches()) { @@ -126,8 +133,20 @@ public class BackupAndRestore { continue; } LocalDateTime backupDate = LocalDateTime.from(DATE_PATTERN.parse(matcher.group(1))); - if (backupDate.isBefore(LocalDateTime.now().minusSeconds(60 * 60 * 24 * 7 * backupMaxAgeInWeeks))) { - logger.info("Deleting backup file {} because it's older than {} weeks", zip, backupMaxAgeInWeeks); + fileToBackupTime.put(zip, backupDate); + } + for (File zip : zips) { + if (!fileToBackupTime.containsKey(zip)) { + continue; + } + LocalDateTime backupDate = fileToBackupTime.get(zip); + if (backupDate.isBefore(LocalDateTime.now().minusSeconds(60L * 60 * 24 * 7 * backupMaxAgeInWeeks))) { + final boolean successfulNewerBackup = fileToBackupTime.entrySet().stream().anyMatch(x -> x.getKey() != zip && x.getValue().isAfter(backupDate)); + if (!successfulNewerBackup) { + logger.warn("No successful backup was made after the creation of {}. Will not delete it.", zip.getAbsolutePath()); + continue; + } + logger.info("Deleting backup file {} because it's older than {} weeks and a newer successful backup exists", zip, backupMaxAgeInWeeks); boolean deleted = zip.delete(); if (!deleted) { logger.warn("Unable to delete old backup file {}", zip.getName()); @@ -138,6 +157,7 @@ public class BackupAndRestore { } } + @Reflective protected File getBackupFolder() { final String backupFolder = configProvider.getBaseConfig().getMain().getBackupFolder(); if (backupFolder.contains(File.separator)) { @@ -164,11 +184,31 @@ public class BackupAndRestore { } - private void backupDatabase(File targetFile) { - String formattedFilepath = targetFile.getAbsolutePath().replace("\\", "/"); - logger.info("Backing up database to " + formattedFilepath); - entityManager.createNativeQuery("BACKUP TO '" + formattedFilepath + "';").executeUpdate(); - logger.debug("Wrote database backup files to {}", targetFile.getAbsolutePath()); + private void backupDatabase(File targetFile, boolean triggeredByUsed) { + final String tempPath; + try { + tempPath = Files.createTempFile("nzbhydra", "zip").toFile().getAbsolutePath().replace("\\", "/"); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + String formattedFilepath = targetFile.getAbsolutePath().replace("\\", "/"); + logger.info("Backing up database to " + formattedFilepath); + //Write a script to ensure that the backed up database is actually valid + final Query nativeQuery = entityManager.createNativeQuery("SCRIPT TO '%s';".formatted(tempPath)); + //If the database is corrupted this command will back it up without exception + entityManager.createNativeQuery("BACKUP TO '" + formattedFilepath + "';").executeUpdate(); + final List resultList = nativeQuery.getResultList(); + logger.debug("Wrote database backup data to {}", targetFile.getAbsolutePath()); + } catch (Exception e) { + logger.info("Deleting invalid backup file {}", targetFile); + targetFile.delete(); + if (!triggeredByUsed) { + final String dbExceptionMessage = Throwables.getCausalChain(e).stream().filter(x -> x instanceof DbException).findFirst().map(Throwable::getMessage).orElse(null); + genericStorage.save("FAILED_BACKUP", new FailedBackupData(dbExceptionMessage)); + } + throw e; + } } private void backupCertificates(FileSystem fileSystem) throws IOException { @@ -205,8 +245,8 @@ public class BackupAndRestore { try { File tempFile = File.createTempFile("nzbhydra-restore", ".zip"); FileUtils.copyInputStreamToFile(inputStream, tempFile); - restoreFromFile(tempFile); tempFile.deleteOnExit(); + restoreFromFile(tempFile); return GenericResponse.ok(); } catch (Exception e) { logger.error("Error while restoring", e); @@ -265,12 +305,5 @@ public class BackupAndRestore { } } - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class BackupEntry { - private String filename; - private Instant creationDate; - } } diff --git a/core/src/main/java/org/nzbhydra/backup/BackupData.java b/core/src/main/java/org/nzbhydra/backup/BackupData.java index da93c5e6e..829947405 100644 --- a/core/src/main/java/org/nzbhydra/backup/BackupData.java +++ b/core/src/main/java/org/nzbhydra/backup/BackupData.java @@ -1,11 +1,12 @@ package org.nzbhydra.backup; -import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.io.Serializable; import java.time.LocalDateTime; -@Data + +@ReflectionMarker public class BackupData implements Serializable { protected LocalDateTime lastBackup; @@ -18,5 +19,11 @@ public class BackupData implements Serializable { this.lastBackup = LocalDateTime.now(); } + public LocalDateTime getLastBackup() { + return lastBackup; + } + public void setLastBackup(LocalDateTime lastBackup) { + this.lastBackup = lastBackup; + } } diff --git a/core/src/main/java/org/nzbhydra/backup/BackupTask.java b/core/src/main/java/org/nzbhydra/backup/BackupTask.java index eb7142cdb..1e430d9bc 100644 --- a/core/src/main/java/org/nzbhydra/backup/BackupTask.java +++ b/core/src/main/java/org/nzbhydra/backup/BackupTask.java @@ -39,7 +39,7 @@ public class BackupTask { int backupEveryXDays = configProvider.getBaseConfig().getMain().getBackupEveryXDays().get(); Optional firstStartOptional = genericStorage.get("FirstStart", LocalDateTime.class); - if (!firstStartOptional.isPresent()) { + if (firstStartOptional.isEmpty()) { logger.debug("First start date time not set (for some reason), aborting backup"); return; } @@ -52,7 +52,7 @@ public class BackupTask { Optional backupData = genericStorage.get(KEY, BackupData.class); - if (!backupData.isPresent()) { + if (backupData.isEmpty()) { logger.debug("Executing first backup: {} days since first start and backup is to be executed every {} days", daysSinceFirstStart, backupEveryXDays); executeBackup(); return; @@ -68,7 +68,7 @@ public class BackupTask { private void executeBackup() { try { logger.info("Starting weekly backup"); - backupAndRestore.backup(); + backupAndRestore.backup(false); genericStorage.save(KEY, new BackupData(LocalDateTime.now(clock))); } catch (Exception e) { logger.error("An error occured while doing a background backup", e); diff --git a/core/src/main/java/org/nzbhydra/backup/BackupWeb.java b/core/src/main/java/org/nzbhydra/backup/BackupWeb.java index 843269dcb..ad3e1de98 100644 --- a/core/src/main/java/org/nzbhydra/backup/BackupWeb.java +++ b/core/src/main/java/org/nzbhydra/backup/BackupWeb.java @@ -1,7 +1,6 @@ package org.nzbhydra.backup; import org.nzbhydra.GenericResponse; -import org.nzbhydra.backup.BackupAndRestore.BackupEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -33,7 +32,7 @@ public class BackupWeb { @Transactional public Object backupAndDownload() throws Exception { try { - File backupFile = backup.backup(); + File backupFile = backup.backup(true); logger.debug("Sending contents of file {}", backupFile.getAbsolutePath()); return ResponseEntity @@ -52,7 +51,7 @@ public class BackupWeb { @Transactional public GenericResponse backupOnly() throws Exception { try { - backup.backup(); + backup.backup(true); return GenericResponse.ok(); } catch (Exception e) { logger.error("Error while creating backup", e); diff --git a/core/src/main/java/org/nzbhydra/backup/FailedBackupData.java b/core/src/main/java/org/nzbhydra/backup/FailedBackupData.java new file mode 100644 index 000000000..d85cfd301 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/backup/FailedBackupData.java @@ -0,0 +1,45 @@ +package org.nzbhydra.backup; + +import org.nzbhydra.springnative.ReflectionMarker; + +import java.io.Serializable; +import java.time.LocalDateTime; + + +@ReflectionMarker +public class FailedBackupData implements Serializable { + + private final LocalDateTime time = LocalDateTime.now(); + private boolean shown; + private String message; + + + public FailedBackupData() { + } + + public FailedBackupData(String message) { + this.message = message; + } + + public boolean isShown() { + return shown; + } + + public void setShown(boolean shown) { + this.shown = shown; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public LocalDateTime getTime() { + return time; + } + + +} diff --git a/core/src/main/java/org/nzbhydra/config/BaseConfig.java b/core/src/main/java/org/nzbhydra/config/BaseConfig.java deleted file mode 100644 index fd0f0b213..000000000 --- a/core/src/main/java/org/nzbhydra/config/BaseConfig.java +++ /dev/null @@ -1,306 +0,0 @@ -package org.nzbhydra.config; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.google.common.base.Joiner; -import lombok.AccessLevel; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.javers.core.metamodel.annotation.DiffIgnore; -import org.nzbhydra.NzbHydra; -import org.nzbhydra.ShutdownEvent; -import org.nzbhydra.config.auth.AuthConfig; -import org.nzbhydra.config.category.CategoriesConfig; -import org.nzbhydra.config.downloading.DownloadingConfig; -import org.nzbhydra.config.indexer.IndexerConfig; -import org.nzbhydra.logging.LoggingMarkers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; - -@Component -@Data -@EqualsAndHashCode(exclude = {"applicationEventPublisher"}, callSuper = false) -public class BaseConfig extends ValidatingConfig { - - private static final Logger logger = LoggerFactory.getLogger(BaseConfig.class); - - public static boolean isProductive = true; - - @Autowired - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - @JsonIgnore - private ApplicationEventPublisher applicationEventPublisher; - @JsonIgnore - private boolean initialized = false; - @JsonIgnore - private ConfigReaderWriter configReaderWriter = new ConfigReaderWriter(); - private AuthConfig auth = new AuthConfig(); - private CategoriesConfig categoriesConfig = new CategoriesConfig(); - private DownloadingConfig downloading = new DownloadingConfig(); - @DiffIgnore - private List indexers = new ArrayList<>(); - private MainConfig main = new MainConfig(); - private SearchingConfig searching = new SearchingConfig(); - private NotificationConfig notificationConfig = new NotificationConfig(); - - @DiffIgnore - private Map genericStorage = new HashMap<>(); - - @JsonIgnore - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - private Lock saveLock = new ReentrantLock(); - @JsonIgnore - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - @ToString.Exclude - private BaseConfig toSave; - @JsonIgnore - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - private TimerTask delayedSaveTimerTask; - - - public void replace(BaseConfig newConfig) { - replace(newConfig, true); - } - - private void replace(BaseConfig newConfig, boolean fireConfigChangedEvent) { - BaseConfig oldBaseConfig = configReaderWriter.getCopy(this); - - main = newConfig.getMain(); - categoriesConfig = newConfig.getCategoriesConfig(); - indexers = newConfig.getIndexers().stream().sorted(Comparator.comparing(IndexerConfig::getName)).collect(Collectors.toList()); - downloading = newConfig.getDownloading(); - searching = newConfig.getSearching(); - auth = newConfig.getAuth(); - genericStorage = newConfig.genericStorage; - notificationConfig = newConfig.notificationConfig; - if (fireConfigChangedEvent) { - ConfigChangedEvent configChangedEvent = new ConfigChangedEvent(this, oldBaseConfig, this); - applicationEventPublisher.publishEvent(configChangedEvent); - } - } - - public void save(boolean saveInstantly) { - saveLock.lock(); - if (saveInstantly) { - logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Saving instantly"); - configReaderWriter.save(this); - toSave = null; - } else { - logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Delaying save"); - toSave = this; - } - saveLock.unlock(); - } - - - @PostConstruct - public void init() throws IOException { - if (!isProductive) { - //Don't overwrite settings from test code - initialized = true; - return; - } - if (initialized) { - //In some cases a call to the server will attempt to restart everything, trying to initialize beans. This - //method is called a second time and an empty / initial config is written - logger.warn("Init method called again. This can only happen during a faulty shutdown"); - return; - } - logger.info("Using data folder {}", NzbHydra.getDataFolder()); - replace(configReaderWriter.loadSavedConfig(), false); - if (main.getApiKey() == null) { - initializeNewConfig(); - } - //Always save config to keep it in sync with base config (remove obsolete settings and add new ones) - configReaderWriter.save(this); - - delayedSaveTimerTask = new TimerTask() { - @Override - public void run() { - saveToSave(); - } - }; - Timer delayedSaveTimer = new Timer("delayedConfigSave", false); - delayedSaveTimer.scheduleAtFixedRate(delayedSaveTimerTask, 10000, 10000); - - initialized = true; - } - - public void load() throws IOException { - replace(configReaderWriter.loadSavedConfig()); - } - - - @EventListener - public void onShutdown(ShutdownEvent event) { - saveToSave(); - } - - private void saveToSave() { - saveLock.lock(); - if (toSave != null) { - logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Executing delayed save"); - configReaderWriter.save(toSave); - toSave = null; - } - saveLock.unlock(); - } - - @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, BaseConfig newConfig, BaseConfig newBaseConfig) { - ConfigValidationResult configValidationResult = new ConfigValidationResult(); - - ConfigValidationResult authValidation = newConfig.getAuth().validateConfig(oldConfig, newConfig.getAuth(), newBaseConfig); - configValidationResult.getErrorMessages().addAll(authValidation.getErrorMessages()); - configValidationResult.getWarningMessages().addAll(authValidation.getWarningMessages()); - configValidationResult.setRestartNeeded(configValidationResult.isRestartNeeded() || authValidation.isRestartNeeded()); - - ConfigValidationResult categoriesValidation = newConfig.getCategoriesConfig().validateConfig(oldConfig, newConfig.getCategoriesConfig(), newBaseConfig); - configValidationResult.getErrorMessages().addAll(categoriesValidation.getErrorMessages()); - configValidationResult.getWarningMessages().addAll(categoriesValidation.getWarningMessages()); - configValidationResult.setRestartNeeded(configValidationResult.isRestartNeeded() || categoriesValidation.isRestartNeeded()); - - ConfigValidationResult mainValidation = newConfig.getMain().validateConfig(oldConfig, newConfig.getMain(), newBaseConfig); - configValidationResult.getErrorMessages().addAll(mainValidation.getErrorMessages()); - configValidationResult.getWarningMessages().addAll(mainValidation.getWarningMessages()); - configValidationResult.setRestartNeeded(configValidationResult.isRestartNeeded() || mainValidation.isRestartNeeded()); - - ConfigValidationResult searchingValidation = newConfig.getSearching().validateConfig(oldConfig, newConfig.getSearching(), newBaseConfig); - configValidationResult.getErrorMessages().addAll(searchingValidation.getErrorMessages()); - configValidationResult.getWarningMessages().addAll(searchingValidation.getWarningMessages()); - configValidationResult.setRestartNeeded(configValidationResult.isRestartNeeded() || searchingValidation.isRestartNeeded()); - - ConfigValidationResult downloadingValidation = newConfig.getDownloading().validateConfig(oldConfig, newConfig.getDownloading(), newBaseConfig); - configValidationResult.getErrorMessages().addAll(downloadingValidation.getErrorMessages()); - configValidationResult.getWarningMessages().addAll(downloadingValidation.getWarningMessages()); - configValidationResult.setRestartNeeded(configValidationResult.isRestartNeeded() || downloadingValidation.isRestartNeeded()); - - for (IndexerConfig indexer : newConfig.getIndexers()) { - ConfigValidationResult indexerValidation = indexer.validateConfig(oldConfig, indexer, newBaseConfig); - configValidationResult.getErrorMessages().addAll(indexerValidation.getErrorMessages()); - configValidationResult.getWarningMessages().addAll(indexerValidation.getWarningMessages()); - configValidationResult.setRestartNeeded(configValidationResult.isRestartNeeded() || indexerValidation.isRestartNeeded()); - } - - validateIndexers(newConfig, configValidationResult); - if (!configValidationResult.getErrorMessages().isEmpty()) { - logger.warn("Config validation returned errors:\n" + Joiner.on("\n").join(configValidationResult.getErrorMessages())); - } - if (!configValidationResult.getWarningMessages().isEmpty()) { - logger.warn("Config validation returned warnings:\n" + Joiner.on("\n").join(configValidationResult.getWarningMessages())); - } - - if (configValidationResult.isRestartNeeded()) { - logger.warn("Settings were changed that require a restart to become effective"); - } - - configValidationResult.setOk(configValidationResult.getErrorMessages().isEmpty()); - - return configValidationResult; - } - - private void validateIndexers(BaseConfig newConfig, ConfigValidationResult configValidationResult) { - if (!newConfig.getIndexers().isEmpty()) { - if (newConfig.getIndexers().stream().noneMatch(x -> x.getState() == IndexerConfig.State.ENABLED)) { - configValidationResult.getWarningMessages().add("No indexers enabled. Searches will return empty results"); - } else if (newConfig.getIndexers().stream().allMatch(x -> x.getSupportedSearchIds().isEmpty())) { - if (newConfig.getSearching().getGenerateQueries() == SearchSourceRestriction.NONE) { - configValidationResult.getWarningMessages().add("No indexer found that supports search IDs. Without query generation searches using search IDs will return empty results."); - } else if (newConfig.getSearching().getGenerateQueries() != SearchSourceRestriction.BOTH) { - String name = newConfig.getSearching().getGenerateQueries() == SearchSourceRestriction.API ? "internal" : "API"; - configValidationResult.getWarningMessages().add("No indexer found that supports search IDs. Without query generation " + name + " searches using search IDs will return empty results."); - } - } - Set indexerNames = new HashSet<>(); - Set duplicateIndexerNames = new HashSet<>(); - - for (IndexerConfig indexer : newConfig.getIndexers()) { - if (!indexerNames.add(indexer.getName())) { - duplicateIndexerNames.add(indexer.getName()); - } - } - if (!duplicateIndexerNames.isEmpty()) { - configValidationResult.getErrorMessages().add("Duplicate indexer names found: " + Joiner.on(", ").join(duplicateIndexerNames)); - } - - - final Set> indexersSameHostAndApikey = new HashSet<>(); - - for (IndexerConfig indexer : newConfig.getIndexers()) { - final Set otherIndexersSameHostAndApiKey = newConfig.getIndexers().stream() - .filter(x -> x != indexer) - .filter(x -> IndexerConfig.isIndexerEquals(x, indexer)) - .map(IndexerConfig::getName) - .collect(Collectors.toSet()); - if (!otherIndexersSameHostAndApiKey.isEmpty()) { - otherIndexersSameHostAndApiKey.add(indexer.getName()); - if (indexersSameHostAndApikey.stream().noneMatch(x -> x.contains(indexer.getName()))) { - indexersSameHostAndApikey.add(otherIndexersSameHostAndApiKey); - final String message = "Found multiple indexers with same host and API key: " + Joiner.on(", ").join(otherIndexersSameHostAndApiKey); - logger.warn(message); - configValidationResult.getWarningMessages().add(message); - } - } - } - - } else { - configValidationResult.getWarningMessages().add("No indexers configured. You won't get any results"); - } - } - - @Override - public BaseConfig prepareForSaving(BaseConfig oldBaseConfig) { - getCategoriesConfig().prepareForSaving(oldBaseConfig); - getDownloading().prepareForSaving(oldBaseConfig); - getSearching().prepareForSaving(oldBaseConfig); - getMain().prepareForSaving(oldBaseConfig); - getMain().getLogging().prepareForSaving(oldBaseConfig); - getAuth().prepareForSaving(oldBaseConfig); - getIndexers().forEach(indexerConfig -> indexerConfig.prepareForSaving(oldBaseConfig)); - - return this; - } - - @Override - public BaseConfig updateAfterLoading() { - getAuth().updateAfterLoading(); - return this; - } - - @Override - public BaseConfig initializeNewConfig() { - getCategoriesConfig().initializeNewConfig(); - getDownloading().initializeNewConfig(); - getSearching().initializeNewConfig(); - getMain().initializeNewConfig(); - getAuth().initializeNewConfig(); - return this; - } - - -} diff --git a/core/src/main/java/org/nzbhydra/config/BaseConfigHandler.java b/core/src/main/java/org/nzbhydra/config/BaseConfigHandler.java new file mode 100644 index 000000000..277cf6f99 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/BaseConfigHandler.java @@ -0,0 +1,147 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.nzbhydra.NzbHydra; +import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.validation.BaseConfigValidator; +import org.nzbhydra.logging.LoggingMarkers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Comparator; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +@Component +public class BaseConfigHandler { + + private static final Logger logger = LoggerFactory.getLogger(BaseConfigHandler.class); + + private final ConfigReaderWriter configReaderWriter = new ConfigReaderWriter(); + + private final Lock saveLock = new ReentrantLock(); + + private BaseConfig toSave; + + private TimerTask delayedSaveTimerTask; + + public boolean initialized = false; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private BaseConfig baseConfig; + @Autowired + private BaseConfigValidator baseConfigValidator; + + @PostConstruct + public void init() throws IOException { + + if (initialized) { + //In some cases a call to the server will attempt to restart everything, trying to initialize beans. This + //method is called a second time and an empty / initial config is written + logger.warn("Init method called again. This can only happen during a faulty shutdown"); + return; + } + logger.info("Using data folder {}", NzbHydra.getDataFolder()); + replace(configReaderWriter.loadSavedConfig(), false); + if (baseConfig.getMain().getApiKey() == null) { + baseConfigValidator.initializeNewConfig(baseConfig); + } + //Always save config to keep it in sync with base config (remove obsolete settings and add new ones) + configReaderWriter.save(baseConfig); + + delayedSaveTimerTask = new TimerTask() { + @Override + public void run() { + saveToSave(); + } + }; + Timer delayedSaveTimer = new Timer("delayedConfigSave", false); + delayedSaveTimer.scheduleAtFixedRate(delayedSaveTimerTask, 10000, 10000); + initialized = true; + } + + public void replace(BaseConfig newConfig) { + replace(newConfig, true); + } + + public void replace(BaseConfig newConfig, boolean fireConfigChangedEvent) { + BaseConfig oldBaseConfig = configReaderWriter.getCopy(baseConfig); + baseConfig.setMain(newConfig.getMain()); + baseConfig.setIndexers(newConfig.getIndexers().stream().sorted(Comparator.comparing(IndexerConfig::getName)).collect(Collectors.toList())); + baseConfig.setCategoriesConfig(newConfig.getCategoriesConfig()); + baseConfig.setSearching(newConfig.getSearching()); + baseConfig.setDownloading(newConfig.getDownloading()); + baseConfig.setAuth(newConfig.getAuth()); + baseConfig.setGenericStorage(newConfig.getGenericStorage()); + baseConfig.setNotificationConfig(newConfig.getNotificationConfig()); + + + if (fireConfigChangedEvent) { + ConfigChangedEvent configChangedEvent = new ConfigChangedEvent(this, oldBaseConfig, newConfig); + applicationEventPublisher.publishEvent(configChangedEvent); + } + } + + public void save(boolean saveInstantly) { + saveLock.lock(); + if (saveInstantly) { + logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Saving instantly"); + configReaderWriter.save(baseConfig); + toSave = null; + } else { + logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Delaying save"); + toSave = baseConfig; + } + saveLock.unlock(); + } + + public void load() throws IOException { + replace(configReaderWriter.loadSavedConfig()); + } + + + @PreDestroy + public void onShutdown() { + saveToSave(); + delayedSaveTimerTask.cancel(); + } + + private void saveToSave() { + saveLock.lock(); + if (toSave != null) { + logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Executing delayed save"); + configReaderWriter.save(toSave); + toSave = null; + } + saveLock.unlock(); + } + + +} diff --git a/core/src/main/java/org/nzbhydra/config/ConfigProvider.java b/core/src/main/java/org/nzbhydra/config/ConfigProvider.java index b7d53fadf..addcf3e5a 100644 --- a/core/src/main/java/org/nzbhydra/config/ConfigProvider.java +++ b/core/src/main/java/org/nzbhydra/config/ConfigProvider.java @@ -2,9 +2,12 @@ package org.nzbhydra.config; import org.nzbhydra.config.indexer.IndexerConfig; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +//BaseConfig must be initialized before we can provide it +@DependsOn("baseConfigHandler") @Component public class ConfigProvider { diff --git a/core/src/main/java/org/nzbhydra/config/ConfigReaderWriter.java b/core/src/main/java/org/nzbhydra/config/ConfigReaderWriter.java index 59f5cf1ba..ea1101fcc 100644 --- a/core/src/main/java/org/nzbhydra/config/ConfigReaderWriter.java +++ b/core/src/main/java/org/nzbhydra/config/ConfigReaderWriter.java @@ -21,8 +21,10 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.base.Charsets; import com.google.common.base.Stopwatch; import com.google.common.base.Strings; -import net.jodah.failsafe.Failsafe; -import net.jodah.failsafe.RetryPolicy; +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; +import dev.failsafe.event.EventListener; +import dev.failsafe.event.ExecutionCompletedEvent; import org.apache.commons.io.IOUtils; import org.nzbhydra.Jackson; import org.nzbhydra.NzbHydra; @@ -38,6 +40,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -47,9 +50,9 @@ public class ConfigReaderWriter { private static final Logger logger = LoggerFactory.getLogger(ConfigReaderWriter.class); - public static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference>() { + public static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference<>() { }; - private final RetryPolicy saveRetryPolicy = new RetryPolicy().retryOn(IOException.class).withDelay(1000, TimeUnit.MILLISECONDS).withMaxRetries(3); + private final RetryPolicy saveRetryPolicy = RetryPolicy.builder().withDelay(Duration.ofMillis(1000)).withMaxRetries(3).handle(IOException.class).build(); public void save(BaseConfig baseConfig) { @@ -81,11 +84,20 @@ public class ConfigReaderWriter { save(converted, buildConfigFileFile()); } + @SuppressWarnings({"Convert2Lambda", "Convert2Diamond"}) // Will not work with diamond protected void save(File targetFile, String configAsYamlString) { + if (NzbHydra.isNativeBuild()) { + return; + } synchronized (Jackson.YAML_MAPPER) { Failsafe.with(saveRetryPolicy) - .onFailure(throwable -> logger.error("Unable to save config", throwable)) - .run(() -> doWrite(targetFile, configAsYamlString)) + .onFailure(new EventListener>() { + @Override + public void accept(ExecutionCompletedEvent event) throws Throwable { + logger.error("Unable to save config", event.getException()); + } + }) + .run(() -> doWrite(targetFile, configAsYamlString)) ; } } @@ -117,14 +129,17 @@ public class ConfigReaderWriter { * * @param yamlFile The path of the file to be created * @return true if initialization was needed - * @throws IOException */ public boolean initializeIfNeeded(File yamlFile) throws IOException { + if (NzbHydra.isNativeBuild()) { + return false; + } if (!yamlFile.exists()) { logger.info("No config file found at {}. Initializing with base config", yamlFile); try { try (InputStream stream = BaseConfig.class.getResource("/config/baseConfig.yml").openStream()) { logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Copying YAML to {}", yamlFile); + yamlFile.getParentFile().mkdirs(); Files.copy(stream, yamlFile.toPath()); return true; } @@ -137,6 +152,9 @@ public class ConfigReaderWriter { } public void validateExistingConfig() { + if (NzbHydra.isNativeBuild()) { + return; + } File configFile = buildConfigFileFile(); if (!configFile.exists()) { logger.debug(LoggingMarkers.CONFIG_READ_WRITE, "Config file {} doesn't exist. Nothing to validate", configFile); diff --git a/core/src/main/java/org/nzbhydra/config/ConfigWeb.java b/core/src/main/java/org/nzbhydra/config/ConfigWeb.java index c1a2d2d91..e2cfbb2b5 100644 --- a/core/src/main/java/org/nzbhydra/config/ConfigWeb.java +++ b/core/src/main/java/org/nzbhydra/config/ConfigWeb.java @@ -1,16 +1,19 @@ package org.nzbhydra.config; +import jakarta.servlet.http.HttpSession; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.GenericResponse; import org.nzbhydra.config.FileSystemBrowser.DirectoryListingRequest; import org.nzbhydra.config.FileSystemBrowser.FileSystemEntry; -import org.nzbhydra.config.ValidatingConfig.ConfigValidationResult; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.safeconfig.SafeConfig; +import org.nzbhydra.config.validation.BaseConfigValidator; +import org.nzbhydra.config.validation.ConfigValidationResult; import org.nzbhydra.indexers.IndexerEntity; import org.nzbhydra.indexers.IndexerRepository; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.web.UrlCalculator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; -import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.HashSet; import java.util.LinkedHashMap; @@ -48,18 +50,22 @@ public class ConfigWeb { private UrlCalculator urlCalculator; @Autowired private IndexerRepository indexerRepository; + @Autowired + private BaseConfigValidator baseConfigValidator; + @Autowired + private BaseConfigHandler baseConfigHandler; private final ConfigReaderWriter configReaderWriter = new ConfigReaderWriter(); @Secured({"ROLE_ADMIN"}) @RequestMapping(value = "/internalapi/config", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public BaseConfig getConfig(HttpSession session) throws IOException { - return configReaderWriter.loadSavedConfig().updateAfterLoading(); + final BaseConfig baseConfig = configReaderWriter.loadSavedConfig(); + return baseConfigValidator.updateAfterLoading(baseConfig); } @Secured({"ROLE_ADMIN"}) @RequestMapping(value = "/internalapi/config", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ConfigValidationResult setConfig(@RequestBody BaseConfig newConfig) throws IOException { - for (PropertySource source : environment.getPropertySources()) { Set propertyNames = new HashSet(); if (source.getSource() instanceof Properties) { @@ -74,13 +80,13 @@ public class ConfigWeb { } logger.info("Received new config"); - newConfig = newConfig.prepareForSaving(configProvider.getBaseConfig()); - ConfigValidationResult result = newConfig.validateConfig(configProvider.getBaseConfig(), newConfig, newConfig); + newConfig = baseConfigValidator.prepareForSaving(configProvider.getBaseConfig(), newConfig); + ConfigValidationResult result = baseConfigValidator.validateConfig(configProvider.getBaseConfig(), newConfig, newConfig); if (result.isOk()) { handleRenamedIndexers(newConfig); - configProvider.getBaseConfig().replace(newConfig); - configProvider.getBaseConfig().save(true); + baseConfigHandler.replace(newConfig); + baseConfigHandler.save(true); result.setNewConfig(configProvider.getBaseConfig()); } return result; @@ -120,7 +126,7 @@ public class ConfigWeb { public GenericResponse reloadConfig() throws IOException { logger.info("Reloading config from file"); try { - configProvider.getBaseConfig().load(); + baseConfigHandler.load(); } catch (IOException e) { return new GenericResponse(false, e.getMessage()); } @@ -150,6 +156,7 @@ public class ConfigWeb { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor private static class ApiHelpResponse { diff --git a/core/src/main/java/org/nzbhydra/config/FileSystemBrowser.java b/core/src/main/java/org/nzbhydra/config/FileSystemBrowser.java index a316e08a0..d27f07bec 100644 --- a/core/src/main/java/org/nzbhydra/config/FileSystemBrowser.java +++ b/core/src/main/java/org/nzbhydra/config/FileSystemBrowser.java @@ -3,6 +3,7 @@ package org.nzbhydra.config; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -47,6 +48,7 @@ public class FileSystemBrowser { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class DirectoryListingRequest { @@ -56,6 +58,7 @@ public class FileSystemBrowser { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class FileSystemEntry { @@ -104,6 +107,7 @@ public class FileSystemBrowser { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class FileSystemSubEntry { diff --git a/core/src/main/java/org/nzbhydra/config/LoggingConfig.java b/core/src/main/java/org/nzbhydra/config/LoggingConfig.java deleted file mode 100644 index 68859809c..000000000 --- a/core/src/main/java/org/nzbhydra/config/LoggingConfig.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.nzbhydra.config; - - -import lombok.Data; -import org.nzbhydra.logging.LoggingMarkers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -@Data -public class LoggingConfig extends ValidatingConfig { - - private static final Logger logger = LoggerFactory.getLogger(LoggingConfig.class); - - @RestartRequired - private String consolelevel; - private HistoryUserInfoType historyUserInfoType = HistoryUserInfoType.NONE; - private boolean logIpAddresses; - private boolean mapIpToHost; - @RestartRequired - private boolean logGc; - private int logMaxHistory; - @RestartRequired - private String logfilelevel; - private boolean logUsername; - private List markersToLog = new ArrayList<>(); - - @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, LoggingConfig newLoggingConfig, BaseConfig newBaseConfig) { - ConfigValidationResult result = new ConfigValidationResult(); - - result.setRestartNeeded(isRestartNeeded(newBaseConfig.getMain().getLogging())); - - if (newBaseConfig.getMain().getLogging().getMarkersToLog().size() > 3) { - result.getWarningMessages().add("You have more than 3 logging markers enabled. This is very rarely useful. Please make sure that this is actually needed. When creating debug infos please only enable those markers requested by the developer."); - } - - return result; - } - - @Override - public LoggingConfig prepareForSaving(BaseConfig oldBaseConfig) { - for (Iterator iterator = markersToLog.iterator(); iterator.hasNext(); ) { - String marker = iterator.next(); - if (Arrays.stream(LoggingMarkers.class.getDeclaredFields()).noneMatch(x -> x.getName().equals(marker))) { - logger.info("Removing logging marker that doesn't exist anymore."); - iterator.remove(); - } - } - return this; - } - - @Override - public LoggingConfig updateAfterLoading() { - return this; - } - - @Override - public LoggingConfig initializeNewConfig() { - return this; - } - -} diff --git a/core/src/main/java/org/nzbhydra/config/MainConfig.java b/core/src/main/java/org/nzbhydra/config/MainConfig.java deleted file mode 100644 index 734e2adc6..000000000 --- a/core/src/main/java/org/nzbhydra/config/MainConfig.java +++ /dev/null @@ -1,243 +0,0 @@ -package org.nzbhydra.config; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonFormat.Shape; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.base.Strings; -import lombok.Data; -import org.javers.core.metamodel.annotation.DiffIgnore; -import org.nzbhydra.NzbHydra; -import org.nzbhydra.config.downloading.ProxyType; -import org.nzbhydra.config.sensitive.SensitiveData; -import org.nzbhydra.debuginfos.DebugInfosProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.io.File; -import java.io.IOException; -import java.math.BigInteger; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Random; - -@ConfigurationProperties("main") -@Data -public class MainConfig extends ValidatingConfig { - - private static final Logger logger = LoggerFactory.getLogger(MainConfig.class); - - private Integer configVersion = 19; - - //Hosting settings - @RestartRequired - private String host = "0.0.0.0"; - @RestartRequired - private int port = 5076; - @RestartRequired - protected String urlBase = null; - - - //Proxy settings - @RestartRequired - @JsonFormat(shape = Shape.STRING) - private ProxyType proxyType = ProxyType.NONE; - @SensitiveData - private String proxyHost = null; - private int proxyPort; - private boolean proxyIgnoreLocal = true; - private List proxyIgnoreDomains = new ArrayList<>(); - @SensitiveData - private String proxyUsername; - @SensitiveData - private String proxyPassword; - - - //Database settings - private String backupFolder; - private Integer backupEveryXDays = 7; - private boolean backupBeforeUpdate = true; - private Integer deleteBackupsAfterWeeks = 4; - - - //History settings - private boolean keepHistory = true; - private Integer keepStatsForWeeks = null; - private Integer keepHistoryForWeeks = null; - - - //SSL settings - @RestartRequired - private boolean ssl = false; - @RestartRequired - private String sslKeyStore = null; - @SensitiveData - @RestartRequired - private String sslKeyStorePassword = null; - - - //Security settings - @RestartRequired - private boolean verifySsl = true; - private boolean disableSslLocally = false; - private List sniDisabledFor = new ArrayList<>(); - private List verifySslDisabledFor = new ArrayList<>(); - - - //Update settings - private boolean updateAutomatically = false; - private boolean updateToPrereleases = false; - private boolean updateCheckEnabled = true; - @JsonProperty("showUpdateBannerOnDocker") - private boolean showUpdateBannerOnUpdatedExternally = true; - private boolean showWhatsNewBanner = true; - - - //Startup / GUI settings - private boolean showNews = true; - private boolean startupBrowser = true; - private boolean welcomeShown = false; - protected String theme; - - - //Database settings - @RestartRequired - private int databaseCompactTime = 15_000; - @RestartRequired - private int databaseRetentionTime = 1000; - @RestartRequired - private int databaseWriteDelay = 5000; - - - //Other settings - @SensitiveData - @DiffIgnore - private String apiKey = null; - private String dereferer = null; - private boolean instanceCounterDownloaded = false; - private String repositoryBase; - private boolean shutdownForRestart = false; - @RestartRequired - private boolean useCsrf = true; - @RestartRequired - private int xmx; - - private LoggingConfig logging = new LoggingConfig(); - - public Optional getUrlBase() { - return Optional.ofNullable(Strings.emptyToNull(urlBase)); - } - - public Optional getDeleteBackupsAfterWeeks() { - return Optional.ofNullable(deleteBackupsAfterWeeks); - } - - public Optional getDereferer() { - return Optional.ofNullable(dereferer); //This must be returned as empty string so that the config can overwrite it - } - - public Optional getBackupEveryXDays() { - return Optional.ofNullable(backupEveryXDays); - } - - @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, MainConfig newMainConfig, BaseConfig newBaseConfig) { - ConfigValidationResult result = new ConfigValidationResult(); - MainConfig oldMain = oldConfig.getMain(); - boolean portChanged = oldMain.getPort() != port; - boolean urlBaseChanged = oldMain.getUrlBase().isPresent() && !oldMain.getUrlBase().get().equals(urlBase); - if (urlBase == null && oldMain.getUrlBase().isPresent() && oldMain.getUrlBase().get().equals("/")) { - urlBaseChanged = false; - } - boolean sslChanged = oldMain.isSsl() != isSsl(); - if (portChanged || urlBaseChanged || sslChanged && !startupBrowser) { - result.getWarningMessages().add("You've made changes that affect Hydra's URL and require a restart. Hydra will try and reload using the new URL when it's back."); - } - if (DebugInfosProvider.isRunInDocker() && !"0.0.0.0".equals(host)) { - result.getWarningMessages().add("You've changed the host but NZBHydra seems to be run in docker. It's recommended to use the host '0.0.0.0'."); - } - - if (!"0.0.0.0".equals(host)) { - try { - boolean reachable = InetAddress.getByName(host).isReachable(1); - if (!reachable) { - result.getWarningMessages().add("The configured host address cannot be reached. Are you sure it is correct?"); - } - } catch (IOException e) { - //Ignore, user will have to know what he does - } - } - if (oldConfig.getMain().getXmx() < 128) { - result.getErrorMessages().add("The JVM memory must be set to at least 128"); - } - - MainConfig newMain = newBaseConfig.getMain(); - if (newMain.getKeepHistoryForWeeks() != null && newMain.getKeepHistoryForWeeks() <= 0) { - result.getErrorMessages().add("Please either delete the value for \"Keep history for\" or set it to a positive value."); - } - if (newMain.getKeepStatsForWeeks() != null && newMain.getKeepStatsForWeeks() <= 0) { - result.getErrorMessages().add("Please either delete the value for \"Keep stats for\" or set it to a positive value."); - } - if (newMain.getKeepStatsForWeeks() != null && newMain.getKeepHistoryForWeeks() != null && newMain.getKeepStatsForWeeks() > newMain.getKeepHistoryForWeeks()) { - result.getErrorMessages().add("Please set the time to keep stats to a value not higher than the time to keep history."); - } - - if (newMain.getBackupFolder() != null) { - final File backupFolderFile; - if (backupFolder.contains(File.separator)) { - backupFolderFile = new File(backupFolder); - } else { - backupFolderFile = new File(NzbHydra.getDataFolder(), backupFolder); - } - if (!backupFolderFile.exists()) { - final boolean created = backupFolderFile.mkdirs(); - if (!created) { - result.getErrorMessages().add("Backup folder " + backupFolder + " does not exist and could not be created"); - } - } - } - - - ConfigValidationResult validationResult = getLogging().validateConfig(oldConfig, getLogging(), newBaseConfig); - result.getWarningMessages().addAll(validationResult.getWarningMessages()); - result.getErrorMessages().addAll(validationResult.getErrorMessages()); - - oldMain = oldMain.prepareForSaving(oldConfig); - result.setRestartNeeded(validationResult.isRestartNeeded() || isRestartNeeded(oldMain)); - result.setOk(validationResult.isOk() && result.isOk()); - - return result; - } - - @Override - public MainConfig prepareForSaving(BaseConfig oldBaseConfig) { - if (!Strings.isNullOrEmpty(urlBase) && (!urlBase.startsWith("/") || urlBase.endsWith("/") || "/".equals(urlBase))) { - if (!urlBase.startsWith("/")) { - urlBase = "/" + urlBase; - } - if (urlBase.endsWith("/")) { - urlBase = urlBase.substring(0, urlBase.length() - 1); - } - if ("/".equals(urlBase) || "".equals(urlBase)) { - urlBase = "/"; - } - setUrlBase(urlBase); - } - return this; - } - - @Override - public MainConfig updateAfterLoading() { - return this; - } - - @Override - public MainConfig initializeNewConfig() { - Random random = new Random(); - setApiKey(new BigInteger(130, random).toString(32).toUpperCase()); - return this; - } - -} diff --git a/core/src/main/java/org/nzbhydra/config/NotificationConfig.java b/core/src/main/java/org/nzbhydra/config/NotificationConfig.java deleted file mode 100644 index fe1b9d350..000000000 --- a/core/src/main/java/org/nzbhydra/config/NotificationConfig.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.nzbhydra.config; - -import joptsimple.internal.Strings; -import lombok.Data; -import org.nzbhydra.config.sensitive.SensitiveData; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.ArrayList; -import java.util.List; - - -@SuppressWarnings("unchecked") -@ConfigurationProperties -@Data -public class NotificationConfig extends ValidatingConfig { - - public enum AppriseType { - NONE, - API, - CLI - } - - private AppriseType appriseType = AppriseType.NONE; - @SensitiveData - private String appriseApiUrl; - @SensitiveData - private String appriseCliPath; - private boolean displayNotifications; - private int displayNotificationsMax; - private List entries = new ArrayList<>(); - private List filterOuts = new ArrayList<>(); - - public NotificationConfig() { - } - - @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, NotificationConfig newConfig, BaseConfig newBaseConfig) { - final List errors = new ArrayList<>(); - final List warnings = new ArrayList<>(); - if (newBaseConfig.getNotificationConfig().getEntries().stream() - .anyMatch(x -> Strings.isNullOrEmpty(x.getAppriseUrls()))) { - errors.add("Make sure all notification entries contain a URL"); - } - - final boolean appriseUrlSet = !Strings.isNullOrEmpty(newBaseConfig.getNotificationConfig().getAppriseApiUrl()); - final boolean anyEntries = newBaseConfig.getNotificationConfig().getEntries().isEmpty(); - - if (anyEntries && !appriseUrlSet) { - warnings.add("No notifications will be sent unless the Apprise API URL is configured."); - } - - return new ConfigValidationResult(true, false, errors, warnings); - } - - @Override - public NotificationConfig prepareForSaving(BaseConfig oldBaseConfig) { - return this; - } - - @Override - public NotificationConfig updateAfterLoading() { - return this; - } - - @Override - public NotificationConfig initializeNewConfig() { - return this; - } - -} diff --git a/core/src/main/java/org/nzbhydra/config/NotificationConfigEntry.java b/core/src/main/java/org/nzbhydra/config/NotificationConfigEntry.java deleted file mode 100644 index e1e9c2f42..000000000 --- a/core/src/main/java/org/nzbhydra/config/NotificationConfigEntry.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.nzbhydra.config; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.nzbhydra.config.sensitive.SensitiveData; -import org.nzbhydra.notifications.NotificationEventType; - - -@SuppressWarnings("unchecked") -@Data -@NoArgsConstructor -@AllArgsConstructor -public class NotificationConfigEntry { - - public enum MessageType { - INFO, - SUCCESS, - WARNING, - FAILURE - } - - @JsonFormat(shape = JsonFormat.Shape.STRING) - private NotificationEventType eventType; - @SensitiveData - private String appriseUrls; - private String titleTemplate; - private String bodyTemplate; - @JsonFormat(shape = JsonFormat.Shape.STRING) - private MessageType messageType; - -} diff --git a/core/src/main/java/org/nzbhydra/config/SearchSourceRestriction.java b/core/src/main/java/org/nzbhydra/config/SearchSourceRestriction.java deleted file mode 100644 index 300b41169..000000000 --- a/core/src/main/java/org/nzbhydra/config/SearchSourceRestriction.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.nzbhydra.config; - -import org.nzbhydra.searching.searchrequests.SearchRequest; - -public enum SearchSourceRestriction { - INTERNAL, - API, - ALL_BUT_RSS, - ONLY_RSS, - BOTH, - NONE; - - public boolean meets(SearchRequest searchRequest) { - if (this == ALL_BUT_RSS && searchRequest.getSource() == SearchRequest.SearchSource.API) { - return searchRequest.getQuery().isPresent() || !searchRequest.getIdentifiers().isEmpty(); - } - if (this == ONLY_RSS && searchRequest.getSource() == SearchRequest.SearchSource.API) { - return !searchRequest.getQuery().isPresent() && searchRequest.getIdentifiers().isEmpty(); - } - return meets(searchRequest.getSource()); - } - - public boolean meets(SearchRequest.SearchSource searchSource) { - return searchSource.name().equals(this.name()) || this == BOTH || this == ALL_BUT_RSS || (this == ONLY_RSS && searchSource == SearchRequest.SearchSource.API); - } - - public boolean isEnabled() { - return this != NONE; - } -} diff --git a/core/src/main/java/org/nzbhydra/config/SearchingConfig.java b/core/src/main/java/org/nzbhydra/config/SearchingConfig.java deleted file mode 100644 index 30bc3d6c5..000000000 --- a/core/src/main/java/org/nzbhydra/config/SearchingConfig.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.nzbhydra.config; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonFormat.Shape; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.google.common.base.Strings; -import lombok.Data; -import org.nzbhydra.indexers.QueryGenerator; -import org.nzbhydra.searching.CustomQueryAndTitleMapping; -import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - - -@SuppressWarnings("unchecked") -@Data -@ConfigurationProperties -public class SearchingConfig extends ValidatingConfig { - - private static final Logger logger = LoggerFactory.getLogger(SearchingConfig.class); - - @JsonFormat(shape = Shape.STRING) - private SearchSourceRestriction applyRestrictions = SearchSourceRestriction.BOTH; - private int coverSize = 128; - private List customMappings = new ArrayList<>(); - private Integer globalCacheTimeMinutes; - private float duplicateAgeThreshold = 2.0F; - private float duplicateSizeThresholdInPercent = 1.0F; - private List forbiddenGroups = new ArrayList<>(); - private List forbiddenPosters = new ArrayList<>(); - private String forbiddenRegex; - private List forbiddenWords = new ArrayList<>(); - private SearchSourceRestriction alwaysConvertIds = SearchSourceRestriction.NONE; - private SearchSourceRestriction generateQueries = SearchSourceRestriction.INTERNAL; - - private QueryGenerator.QueryFormat generateQueriesFormat = QueryGenerator.QueryFormat.TITLE; - @JsonFormat(shape = Shape.STRING) - private SearchSourceRestriction idFallbackToQueryGeneration = SearchSourceRestriction.NONE; - private boolean ignorePassworded = false; - private boolean ignoreTemporarilyDisabled = false; - private boolean ignoreLoadLimitingForInternalSearches = false; - private int keepSearchResultsForDays = 3; - private String language = "en"; - private List languagesToKeep = new ArrayList<>(); - private boolean loadAllCachedOnInternal; - private int loadLimitInternal = 100; - private Integer maxAge; - private Integer minSeeders; - @JsonSetter() - private List removeTrailing = new ArrayList<>(); - private boolean replaceUmlauts = false; - private String requiredRegex; - private List requiredWords = new ArrayList<>(); - private boolean sendTorznabCategories = true; - private boolean showQuickFilterButtons = true; - private boolean alwaysShowQuickFilterButtons = false; - private List customQuickFilterButtons = new ArrayList<>(); - private List preselectQuickFilterButtons = new ArrayList<>(); - private Integer timeout = 30; - private boolean transformNewznabCategories = true; - private String userAgent = "NZBHydra2"; - private List userAgents = new ArrayList<>(Arrays.asList("Mozilla", "Sonarr", "Radarr", "CouchPotato", "LazyLibrarian", "Lidarr", "NZBGet", "sabNZBd", "Readarr")); - private boolean useOriginalCategories = false; - private boolean wrapApiErrors = false; - - public SearchingConfig() { - } - - public Optional getGlobalCacheTimeMinutes() { - return Optional.ofNullable(globalCacheTimeMinutes); - } - - public Optional getMaxAge() { - return Optional.ofNullable(maxAge); - } - - public Optional getForbiddenRegex() { - return Optional.ofNullable(Strings.emptyToNull(forbiddenRegex)); - } - - public Optional getRequiredRegex() { - return Optional.ofNullable(Strings.emptyToNull(requiredRegex)); - } - - public Optional getUserAgent() { - return Optional.ofNullable(Strings.emptyToNull(userAgent)); - } - - public Optional getLanguage() { - return Optional.ofNullable(Strings.emptyToNull(language)); - } - - - @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, SearchingConfig newConfig, BaseConfig newBaseConfig) { - List errors = new ArrayList<>(); - List warnings = new ArrayList<>(); - checkRegex(errors, requiredRegex, "The required regex in \"Searching\" is invalid"); - checkRegex(errors, forbiddenRegex, "The forbidden in \"Searching\" is invalid"); - - if (applyRestrictions == SearchSourceRestriction.NONE) { - if (!getRequiredWords().isEmpty() || !getForbiddenWords().isEmpty()) { - warnings.add("You selected not to apply any word restrictions in \"Searching\" but supplied forbidden or required words there"); - } - if (getRequiredRegex().isPresent() || getForbiddenRegex().isPresent()) { - warnings.add("You selected not to apply any word restrictions in \"Searching\" but supplied a forbidden or required regex there"); - } - } - final CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping(newBaseConfig); - final SearchRequest searchRequest = new SearchRequest(); - searchRequest.setTitle("test title"); - searchRequest.setQuery("test query"); - for (CustomQueryAndTitleMapping.Mapping customMapping : newConfig.getCustomMappings()) { - try { - customQueryAndTitleMapping.mapSearchRequest(searchRequest, Collections.singletonList(customMapping)); - } catch (Exception e) { - errors.add(String.format("Unable to process mapping %s:}\n%s", customMapping.toString(), e.getMessage())); - } - if (customMapping.getFrom().contains("{episode:")) { - errors.add("The group 'episode' is not allowed in custom mapping input patterns."); - } - if (customMapping.getFrom().contains("{season:")) { - errors.add("The group 'season' is not allowed in custom mapping input patterns."); - } - } - final List emptyTrailing = (newConfig.getRemoveTrailing().stream().filter(Strings::isNullOrEmpty)).collect(Collectors.toList()); - if (!emptyTrailing.isEmpty()) { - errors.add("Trailing values to remove contains empty values"); - } - - return new ConfigValidationResult(errors.isEmpty(), false, errors, warnings); - } - - @Override - public SearchingConfig prepareForSaving(BaseConfig oldBaseConfig) { - final Set customQuickfilterNames = customQuickFilterButtons.stream().map(x -> x.split("=")[0]).collect(Collectors.toSet()); - for (Iterator iterator = getPreselectQuickFilterButtons().iterator(); iterator.hasNext(); ) { - String preselectQuickFilterButton = iterator.next(); - final String[] split = preselectQuickFilterButton.split("\\|"); - if ("custom".equals(split[0]) && !customQuickfilterNames.contains(split[0])) { - logger.info("Custom quickfilter {} doesn't exist anymore, removing it from list of filters to preselect.", preselectQuickFilterButton); - iterator.remove(); - } - } - return this; - } - - @Override - public SearchingConfig updateAfterLoading() { - return this; - } - - @Override - public SearchingConfig initializeNewConfig() { - return this; - } - -} diff --git a/core/src/main/java/org/nzbhydra/config/ValidatingConfig.java b/core/src/main/java/org/nzbhydra/config/ValidatingConfig.java deleted file mode 100644 index 9f9e9b542..000000000 --- a/core/src/main/java/org/nzbhydra/config/ValidatingConfig.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.nzbhydra.config; - -import com.google.common.base.Strings; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -public abstract class ValidatingConfig { - - private static final Logger logger = LoggerFactory.getLogger(ValidatingConfig.class); - - /** - * @param oldConfig old config state (e.g. to compare what has changed) - * @param newConfig the new config. Will always be the same object as the one on which the method was called - * @param newBaseConfig - * @return a list of error messages or an empty list when everything is fine - */ - public abstract ConfigValidationResult validateConfig(BaseConfig oldConfig, T newConfig, BaseConfig newBaseConfig); - - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class ConfigValidationResult { - private boolean ok = true; - private boolean restartNeeded; - private List errorMessages = new ArrayList<>(); - private List warningMessages = new ArrayList<>(); - private BaseConfig newConfig; - - public ConfigValidationResult(boolean ok, boolean restartNeeded, List errorMessages, List warningMessages) { - this.ok = ok; - this.restartNeeded = restartNeeded; - this.errorMessages.addAll(new HashSet<>(errorMessages)); - this.warningMessages.addAll(new HashSet<>(warningMessages)); - } - } - - protected void checkRegex(List errorMessages, String regex, String errorMessage) { - if (!Strings.isNullOrEmpty(regex)) { - try { - Pattern.compile(regex); - } catch (PatternSyntaxException e) { - errorMessages.add(errorMessage); - } - } - } - - /** - * Detects if any setting was changed that requires a restart to be effective - * - * @param configToCompare the old config (its settings will be compared with the ones from the calling instance) - * @return - */ - protected boolean isRestartNeeded(Object configToCompare) { - for (Field field : configToCompare.getClass().getDeclaredFields()) { - if (field.isAnnotationPresent(RestartRequired.class)) { - try { - //PropertyDescriptor doesn't work for some reason, this is just as fine for what we need - String getterName = (field.getType() == Boolean.class || field.getType() == boolean.class ? "is" : "get") + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); - Method method = configToCompare.getClass().getDeclaredMethod(getterName); - Object oldValue = method.invoke(configToCompare); - Object newValue = method.invoke(this); - - if (!Objects.equals(oldValue, newValue)) { - logger.debug("Restart needed because field {} has changed", field.getName()); - return true; - } - } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { - logger.error("Unable to determine if field '{}' in class {} was changed", field.getName(), configToCompare.getClass().getName()); - } - } - } - return false; - } - - /** - * Called before the config is saved after the user made some changes. Use this to convert data, e.g. passwords. - * - * @param oldBaseConfig - */ - public abstract T prepareForSaving(BaseConfig oldBaseConfig); - - /** - * Called before the config is transferred to the GUI. Use this to prepare data, e.g. passwords. - */ - public abstract T updateAfterLoading(); - - /** - * Called for a new config to initialize itself - */ - public abstract T initializeNewConfig(); - - - - -} diff --git a/core/src/main/java/org/nzbhydra/config/auth/AuthConfig.java b/core/src/main/java/org/nzbhydra/config/auth/AuthConfig.java deleted file mode 100644 index 01f97268d..000000000 --- a/core/src/main/java/org/nzbhydra/config/auth/AuthConfig.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.config.auth; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonFormat.Shape; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.google.common.base.Joiner; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.javers.core.metamodel.annotation.DiffIgnore; -import org.nzbhydra.config.BaseConfig; -import org.nzbhydra.config.RestartRequired; -import org.nzbhydra.config.ValidatingConfig; -import org.nzbhydra.config.sensitive.SensitiveData; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Data -@ConfigurationProperties -@EqualsAndHashCode -public class AuthConfig extends ValidatingConfig { - - @JsonFormat(shape = Shape.STRING) - @RestartRequired - private AuthType authType; - private boolean rememberUsers = true; - private int rememberMeValidityDays; - @SensitiveData - private String authHeader; - private List authHeaderIpRanges = new ArrayList<>(); - private boolean restrictAdmin = false; - private boolean restrictDetailsDl = false; - private boolean restrictIndexerSelection = false; - private boolean restrictSearch = false; - private boolean restrictStats = false; - private boolean allowApiStats = true; - - @DiffIgnore - private List users = new ArrayList<>(); - - @JsonIgnore - public boolean isAuthConfigured() { - return authType != AuthType.NONE; - } - - @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, AuthConfig newConfig, BaseConfig newBaseConfig) { - List errors = new ArrayList<>(); - List warnings = new ArrayList<>(); - if (authType != AuthType.NONE && users.isEmpty()) { - errors.add("You've enabled security but not defined any users"); - } else if (authType != AuthType.NONE && restrictAdmin && users.stream().noneMatch(UserAuthConfig::isMaySeeAdmin)) { - errors.add("You've restricted admin access but no user has admin rights"); - } else if (authType != AuthType.NONE && !restrictSearch && !restrictAdmin) { - errors.add("You haven't enabled any access restrictions. Auth will not take any effect"); - } - Set usernames = new HashSet<>(); - List duplicateUsernames = new ArrayList<>(); - for (UserAuthConfig user : users) { - if (usernames.contains(user.getUsername())) { - duplicateUsernames.add(user.getUsername()); - } - usernames.add(user.getUsername()); - } - if (!duplicateUsernames.isEmpty()) { - errors.add("The following user names are not unique: " + Joiner.on(", ").join(duplicateUsernames)); - } - - if (!authHeaderIpRanges.isEmpty()) { - authHeaderIpRanges.forEach(x -> { - Matcher matcher = Pattern.compile("^((?:[0-9]{1,3}\\.){3}[0-9]{1,3}(-(?:[0-9]{1,3}\\.){3}[0-9]{1,3})?,?)+$").matcher(x); - if (!matcher.matches()) { - errors.add("IP range " + x + " is invalid"); - } - }); - } - - return new ConfigValidationResult(errors.isEmpty(), isRestartNeeded(oldConfig.getAuth()), errors, warnings); - } - - @Override - public AuthConfig prepareForSaving(BaseConfig oldBaseConfig) { - getUsers().forEach(userAuthConfig -> userAuthConfig.prepareForSaving(oldBaseConfig)); - return this; - } - - @Override - public AuthConfig updateAfterLoading() { - getUsers().forEach(ValidatingConfig::updateAfterLoading); - return this; - } - - @Override - public AuthConfig initializeNewConfig() { - return this; - } -} diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep006to007.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep006to007.java index 9aa0069df..5025f8b3e 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep006to007.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep006to007.java @@ -22,7 +22,6 @@ import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep006to007 implements ConfigMigrationStep { private static final Logger logger = LoggerFactory.getLogger(ConfigMigrationStep006to007.class); diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep007to008.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep007to008.java index 666f5ccd5..77fed66ba 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep007to008.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep007to008.java @@ -21,7 +21,6 @@ import org.slf4j.LoggerFactory; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep007to008 implements ConfigMigrationStep { private static final Logger logger = LoggerFactory.getLogger(ConfigMigrationStep007to008.class); diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep008to009.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep008to009.java index 2cfc06b4e..e384aa891 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep008to009.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep008to009.java @@ -21,7 +21,6 @@ import org.slf4j.LoggerFactory; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep008to009 implements ConfigMigrationStep { private static final Logger logger = LoggerFactory.getLogger(ConfigMigrationStep008to009.class); diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep009to010.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep009to010.java index f455159a3..dff0d01fd 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep009to010.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep009to010.java @@ -19,7 +19,6 @@ package org.nzbhydra.config.migration; import java.util.ArrayList; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep009to010 implements ConfigMigrationStep { @Override diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep010to011.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep010to011.java index f2f4cd8fd..c9c80f872 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep010to011.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep010to011.java @@ -18,7 +18,6 @@ package org.nzbhydra.config.migration; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep010to011 implements ConfigMigrationStep { @Override diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep011to012.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep011to012.java index 91c89167f..200c68b57 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep011to012.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep011to012.java @@ -21,7 +21,6 @@ import org.slf4j.LoggerFactory; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep011to012 implements ConfigMigrationStep { private static final Logger logger = LoggerFactory.getLogger(ConfigMigration.class); diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep012to013.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep012to013.java index 6a3e3e1b3..aa2dcd1b4 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep012to013.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep012to013.java @@ -21,7 +21,6 @@ import org.slf4j.LoggerFactory; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep012to013 implements ConfigMigrationStep { private static final Logger logger = LoggerFactory.getLogger(ConfigMigration.class); diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep013to014.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep013to014.java index d13f92996..885413938 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep013to014.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep013to014.java @@ -21,7 +21,6 @@ import org.slf4j.LoggerFactory; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep013to014 implements ConfigMigrationStep { private static final Logger logger = LoggerFactory.getLogger(ConfigMigration.class); diff --git a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep018to019.java b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep018to019.java index c55fa40d7..537a9a45e 100644 --- a/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep018to019.java +++ b/core/src/main/java/org/nzbhydra/config/migration/ConfigMigrationStep018to019.java @@ -21,7 +21,6 @@ import org.slf4j.LoggerFactory; import java.util.Map; -@SuppressWarnings("unchecked") public class ConfigMigrationStep018to019 implements ConfigMigrationStep { private static final Logger logger = LoggerFactory.getLogger(ConfigMigration.class); diff --git a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeCategory.java b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeCategory.java index 5e66fab9f..9edd99338 100644 --- a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeCategory.java +++ b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeCategory.java @@ -2,9 +2,11 @@ package org.nzbhydra.config.safeconfig; import lombok.Data; import org.nzbhydra.config.category.Category; +import org.nzbhydra.springnative.ReflectionMarker; //Only needed because I can't convince Thymeleaf to serialize enums as their names @Data +@ReflectionMarker public class SafeCategory { private final boolean mayBeSelected; diff --git a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeConfig.java b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeConfig.java index 4a5eb9222..c48197ac7 100644 --- a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeConfig.java +++ b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeConfig.java @@ -3,11 +3,13 @@ package org.nzbhydra.config.safeconfig; import lombok.Data; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.auth.AuthType; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.List; import java.util.stream.Collectors; @Data +@ReflectionMarker public class SafeConfig { private SafeCategoriesConfig categoriesConfig; diff --git a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeDownloaderConfig.java b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeDownloaderConfig.java index 41e8aa0a3..6521904e3 100644 --- a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeDownloaderConfig.java +++ b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeDownloaderConfig.java @@ -2,8 +2,10 @@ package org.nzbhydra.config.safeconfig; import lombok.Data; import org.nzbhydra.config.downloading.DownloaderConfig; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker public class SafeDownloaderConfig { private String defaultCategory; diff --git a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeIndexerConfig.java b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeIndexerConfig.java index b37832d1b..e2ef29852 100644 --- a/core/src/main/java/org/nzbhydra/config/safeconfig/SafeIndexerConfig.java +++ b/core/src/main/java/org/nzbhydra/config/safeconfig/SafeIndexerConfig.java @@ -3,10 +3,12 @@ package org.nzbhydra.config.safeconfig; import lombok.Data; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.List; @Data +@ReflectionMarker public class SafeIndexerConfig { private String name; diff --git a/core/src/main/java/org/nzbhydra/config/sensitive/SensitiveDataHidingSerializer.java b/core/src/main/java/org/nzbhydra/config/sensitive/SensitiveDataHidingSerializer.java index e7c69be8d..0f4b97f6a 100644 --- a/core/src/main/java/org/nzbhydra/config/sensitive/SensitiveDataHidingSerializer.java +++ b/core/src/main/java/org/nzbhydra/config/sensitive/SensitiveDataHidingSerializer.java @@ -16,8 +16,7 @@ public class SensitiveDataHidingSerializer extends JsonSerializer { @Override public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { String toWrite = ""; - if (value instanceof Optional) { - Optional optional = (Optional) value; + if (value instanceof Optional optional) { toWrite = optional.isPresent() ? "" : ""; } logger.debug("Hiding sensitive data in config setting \"{}\"", gen.getOutputContext().getCurrentName()); diff --git a/core/src/main/java/org/nzbhydra/config/validation/AuthConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/AuthConfigValidator.java new file mode 100644 index 000000000..b9a629c1c --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/AuthConfigValidator.java @@ -0,0 +1,92 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import com.google.common.base.Joiner; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.auth.AuthConfig; +import org.nzbhydra.config.auth.AuthType; +import org.nzbhydra.config.auth.UserAuthConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +public class AuthConfigValidator implements ConfigValidator { + + @Autowired + private UserAuthConfigValidator userAuthConfigValidator; + + @Override + public boolean doesValidate(Class clazz) { + return clazz == AuthConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, AuthConfig newConfig) { + List errors = new ArrayList<>(); + List warnings = new ArrayList<>(); + if (newConfig.getAuthType() != AuthType.NONE && newConfig.getUsers().isEmpty()) { + errors.add("You've enabled security but not defined any users"); + } else if (newConfig.getAuthType() != AuthType.NONE && newConfig.isRestrictAdmin() && newConfig.getUsers().stream().noneMatch(UserAuthConfig::isMaySeeAdmin)) { + errors.add("You've restricted admin access but no user has admin rights"); + } else if (newConfig.getAuthType() != AuthType.NONE && !newConfig.isRestrictSearch() && !newConfig.isRestrictAdmin()) { + errors.add("You haven't enabled any access restrictions. Auth will not take any effect"); + } + Set usernames = new HashSet<>(); + List duplicateUsernames = new ArrayList<>(); + for (UserAuthConfig user : newConfig.getUsers()) { + if (usernames.contains(user.getUsername())) { + duplicateUsernames.add(user.getUsername()); + } + usernames.add(user.getUsername()); + } + if (!duplicateUsernames.isEmpty()) { + errors.add("The following user names are not unique: " + Joiner.on(", ").join(duplicateUsernames)); + } + + if (!newConfig.getAuthHeaderIpRanges().isEmpty()) { + newConfig.getAuthHeaderIpRanges().forEach(x -> { + Matcher matcher = Pattern.compile("^((?:[0-9]{1,3}\\.){3}[0-9]{1,3}(-(?:[0-9]{1,3}\\.){3}[0-9]{1,3})?,?)+$").matcher(x); + if (!matcher.matches()) { + errors.add("IP range " + x + " is invalid"); + } + }); + } + + return new ConfigValidationResult(errors.isEmpty(), ConfigValidationTools.isRestartNeeded(oldBaseConfig.getAuth(), newConfig), errors, warnings); + } + + @Override + public AuthConfig prepareForSaving(BaseConfig oldBaseConfig, AuthConfig newAuthConfig) { + newAuthConfig.getUsers().forEach(userAuthConfig -> userAuthConfigValidator.prepareForSaving(oldBaseConfig, userAuthConfig)); + return new AuthConfig(); + } + + @Override + public AuthConfig updateAfterLoading(AuthConfig newAuthConfig) { + newAuthConfig.getUsers().forEach(userAuthConfig -> userAuthConfigValidator.updateAfterLoading(userAuthConfig)); + return newAuthConfig; + } + +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/BaseConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/BaseConfigValidator.java new file mode 100644 index 000000000..3c5fcd79f --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/BaseConfigValidator.java @@ -0,0 +1,172 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import com.google.common.base.Joiner; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.SearchSourceRestriction; +import org.nzbhydra.config.indexer.IndexerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@SuppressWarnings("unchecked") +@Component +public class BaseConfigValidator implements ConfigValidator { + + private static final Logger logger = LoggerFactory.getLogger(BaseConfigValidator.class); + + @Autowired + private CategoriesConfigValidator categoriesConfigValidator; + @Autowired + private DownloadingConfigValidator downloadingConfigValidator; + @Autowired + private SearchingConfigValidator searchingConfigValidator; + @Autowired + private MainConfigValidator mainConfigValidator; + @Autowired + private AuthConfigValidator authConfigValidator; + @Autowired + private IndexerConfigValidator indexerConfigValidator; + @Autowired + private List configValidatorList; + + @Override + public boolean doesValidate(Class clazz) { + return clazz == BaseConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, BaseConfig newConfig) { + final ConfigValidationResult configValidationResult = new ConfigValidationResult(); + final List configs = new ArrayList<>(Arrays.asList( + newBaseConfig.getMain(), + newBaseConfig.getSearching(), + newBaseConfig.getDownloading(), + newBaseConfig.getCategoriesConfig(), + newBaseConfig.getAuth() + )); + configs.addAll(newConfig.getIndexers()); + for (Object config : configs) { + final ConfigValidator validator = configValidatorList.stream().filter(x -> x.doesValidate(config.getClass())).findFirst().orElseThrow(); + final ConfigValidationResult result = validator.validateConfig(oldBaseConfig, newBaseConfig, config); + configValidationResult.getErrorMessages().addAll(result.getErrorMessages()); + configValidationResult.getWarningMessages().addAll(result.getWarningMessages()); + } + validateIndexers(newConfig, configValidationResult); + + + if (!configValidationResult.getErrorMessages().isEmpty()) { + logger.warn("Config validation returned errors:\n" + Joiner.on("\n").join(configValidationResult.getErrorMessages())); + } + if (!configValidationResult.getWarningMessages().isEmpty()) { + logger.warn("Config validation returned warnings:\n" + Joiner.on("\n").join(configValidationResult.getWarningMessages())); + } + + if (configValidationResult.isRestartNeeded()) { + logger.warn("Settings were changed that require a restart to become effective"); + } + + configValidationResult.setOk(configValidationResult.getErrorMessages().isEmpty()); + + return configValidationResult; + } + + private void validateIndexers(BaseConfig newConfig, ConfigValidationResult configValidationResult) { + if (!newConfig.getIndexers().isEmpty()) { + if (newConfig.getIndexers().stream().noneMatch(x -> x.getState() == IndexerConfig.State.ENABLED)) { + configValidationResult.getWarningMessages().add("No indexers enabled. Searches will return empty results"); + } else if (newConfig.getIndexers().stream().allMatch(x -> x.getSupportedSearchIds().isEmpty())) { + if (newConfig.getSearching().getGenerateQueries() == SearchSourceRestriction.NONE) { + configValidationResult.getWarningMessages().add("No indexer found that supports search IDs. Without query generation searches using search IDs will return empty results."); + } else if (newConfig.getSearching().getGenerateQueries() != SearchSourceRestriction.BOTH) { + String name = newConfig.getSearching().getGenerateQueries() == SearchSourceRestriction.API ? "internal" : "API"; + configValidationResult.getWarningMessages().add("No indexer found that supports search IDs. Without query generation " + name + " searches using search IDs will return empty results."); + } + } + Set indexerNames = new HashSet<>(); + Set duplicateIndexerNames = new HashSet<>(); + + for (IndexerConfig indexer : newConfig.getIndexers()) { + if (!indexerNames.add(indexer.getName())) { + duplicateIndexerNames.add(indexer.getName()); + } + } + if (!duplicateIndexerNames.isEmpty()) { + configValidationResult.getErrorMessages().add("Duplicate indexer names found: " + Joiner.on(", ").join(duplicateIndexerNames)); + } + + + final Set> indexersSameHostAndApikey = new HashSet<>(); + + for (IndexerConfig indexer : newConfig.getIndexers()) { + final Set otherIndexersSameHostAndApiKey = newConfig.getIndexers().stream() + .filter(x -> x != indexer) + .filter(x -> IndexerConfig.isIndexerEquals(x, indexer)) + .map(IndexerConfig::getName) + .collect(Collectors.toSet()); + if (!otherIndexersSameHostAndApiKey.isEmpty()) { + otherIndexersSameHostAndApiKey.add(indexer.getName()); + if (indexersSameHostAndApikey.stream().noneMatch(x -> x.contains(indexer.getName()))) { + indexersSameHostAndApikey.add(otherIndexersSameHostAndApiKey); + final String message = "Found multiple indexers with same host and API key: " + Joiner.on(", ").join(otherIndexersSameHostAndApiKey); + logger.warn(message); + configValidationResult.getWarningMessages().add(message); + } + } + } + + } else { + configValidationResult.getWarningMessages().add("No indexers configured. You won't get any results"); + } + } + + @Override + public BaseConfig prepareForSaving(BaseConfig oldBaseConfig, BaseConfig newConfig) { + categoriesConfigValidator.prepareForSaving(oldBaseConfig, newConfig.getCategoriesConfig()); + downloadingConfigValidator.prepareForSaving(oldBaseConfig, newConfig.getDownloading()); + searchingConfigValidator.prepareForSaving(oldBaseConfig, newConfig.getSearching()); + mainConfigValidator.prepareForSaving(oldBaseConfig, newConfig.getMain()); + authConfigValidator.prepareForSaving(oldBaseConfig, newConfig.getAuth()); + newConfig.getIndexers().forEach(x -> indexerConfigValidator.prepareForSaving(oldBaseConfig, x)); + return newConfig; + } + + @Override + public BaseConfig updateAfterLoading(BaseConfig newConfig) { + authConfigValidator.updateAfterLoading(newConfig.getAuth()); + return newConfig; + } + + @Override + public BaseConfig initializeNewConfig(BaseConfig newConfig) { + categoriesConfigValidator.initializeNewConfig(newConfig.getCategoriesConfig()); + downloadingConfigValidator.initializeNewConfig(newConfig.getDownloading()); + searchingConfigValidator.initializeNewConfig(newConfig.getSearching()); + mainConfigValidator.initializeNewConfig(newConfig.getMain()); + authConfigValidator.initializeNewConfig(newConfig.getAuth()); + return newConfig; + } +} diff --git a/core/src/main/java/org/nzbhydra/config/category/CategoriesConfig.java b/core/src/main/java/org/nzbhydra/config/validation/CategoriesConfigValidator.java similarity index 62% rename from core/src/main/java/org/nzbhydra/config/category/CategoriesConfig.java rename to core/src/main/java/org/nzbhydra/config/validation/CategoriesConfigValidator.java index 9d0ce7190..1d7b40220 100644 --- a/core/src/main/java/org/nzbhydra/config/category/CategoriesConfig.java +++ b/core/src/main/java/org/nzbhydra/config/validation/CategoriesConfigValidator.java @@ -1,5 +1,5 @@ /* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,50 +14,38 @@ * limitations under the License. */ -package org.nzbhydra.config.category; - +package org.nzbhydra.config.validation; import com.google.common.base.Joiner; -import com.google.common.base.MoreObjects; -import lombok.Data; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.SearchSourceRestriction; -import org.nzbhydra.config.ValidatingConfig; -import org.nzbhydra.config.category.Category.Subtype; +import org.nzbhydra.config.category.CategoriesConfig; +import org.nzbhydra.config.category.Category; import org.nzbhydra.searching.CategoryProvider; +import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -import static org.nzbhydra.searching.dtoseventsenums.SearchType.SEARCH; +import static org.nzbhydra.config.validation.ConfigValidationTools.checkRegex; -@Data -public class CategoriesConfig extends ValidatingConfig { - - public final static Category allCategory = new Category("All"); - - static { - allCategory.setApplyRestrictionsType(SearchSourceRestriction.NONE); - allCategory.setIgnoreResultsFrom(SearchSourceRestriction.NONE); - allCategory.setMayBeSelected(true); - allCategory.setSearchType(SEARCH); - allCategory.setSubtype(Subtype.ALL); - } - - private boolean enableCategorySizes = true; - private List categories = new ArrayList<>(); - private String defaultCategory = "All"; +@Component +public class CategoriesConfigValidator implements ConfigValidator { @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, CategoriesConfig newConfig, BaseConfig newBaseConfig) { + public boolean doesValidate(Class clazz) { + return clazz == CategoriesConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, CategoriesConfig newConfig) { ArrayList errors = new ArrayList<>(); ArrayList warnings = new ArrayList<>(); - for (Category category : categories) { + for (Category category : newConfig.getCategories()) { if (category.getNewznabCategories() == null || category.getNewznabCategories().isEmpty()) { errors.add("Category \"" + category.getName() + "\" does not have any newznab categories configured"); } else { @@ -86,49 +74,16 @@ public class CategoriesConfig extends ValidatingConfig { } } } - List allNewznabCategories = categories.stream().flatMap(x -> x.getNewznabCategories().stream().flatMap(Collection::stream)).collect(Collectors.toList()); + List allNewznabCategories = newConfig.getCategories().stream().flatMap(x -> x.getNewznabCategories().stream().flatMap(Collection::stream)).toList(); List duplicateNewznabCategories = allNewznabCategories.stream().filter(x -> Collections.frequency(allNewznabCategories, 1) > 1).collect(Collectors.toList()); if (!duplicateNewznabCategories.isEmpty()) { errors.add("The following newznab categories are assigned to multiple indexers: " + Joiner.on(", ").join(duplicateNewznabCategories)); } - if (!"All".equals(newConfig.getDefaultCategory()) && categories.stream().noneMatch(x -> x.getName().equals(newConfig.getDefaultCategory()))) { + if (!"All".equals(newConfig.getDefaultCategory()) && newConfig.getCategories().stream().noneMatch(x -> x.getName().equals(newConfig.getDefaultCategory()))) { errors.add("Category \"" + newConfig.getDefaultCategory() + "\" set as default category but no such category exists"); } return new ConfigValidationResult(errors.isEmpty(), false, errors, warnings); } - - public void setCategories(List categories) { - categories.sort(Comparator.comparing(Category::getName)); - this.categories = categories; - } - - public List withoutAll() { - return categories.stream().filter(x -> !allCategory.equals(x)).collect(Collectors.toList()); - } - - @Override - public CategoriesConfig prepareForSaving(BaseConfig oldBaseConfig) { - return this; - } - - @Override - public CategoriesConfig updateAfterLoading() { - return this; - } - - @Override - public CategoriesConfig initializeNewConfig() { - return this; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("enableCategorySizes", enableCategorySizes) - .add("categories", categories) - .add("defaultCategory", defaultCategory) - .toString(); - } } diff --git a/core/src/main/java/org/nzbhydra/config/validation/ConfigValidationResult.java b/core/src/main/java/org/nzbhydra/config/validation/ConfigValidationResult.java new file mode 100644 index 000000000..d2a48feab --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/ConfigValidationResult.java @@ -0,0 +1,46 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.springnative.ReflectionMarker; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +@Data +@ReflectionMarker +@AllArgsConstructor +@NoArgsConstructor +public class ConfigValidationResult { + private boolean ok = true; + private boolean restartNeeded; + private List errorMessages = new ArrayList<>(); + private List warningMessages = new ArrayList<>(); + private BaseConfig newConfig; + + public ConfigValidationResult(boolean ok, boolean restartNeeded, List errorMessages, List warningMessages) { + this.ok = ok; + this.restartNeeded = restartNeeded; + this.errorMessages.addAll(new HashSet<>(errorMessages)); + this.warningMessages.addAll(new HashSet<>(warningMessages)); + } +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/ConfigValidationTools.java b/core/src/main/java/org/nzbhydra/config/validation/ConfigValidationTools.java new file mode 100644 index 000000000..0f0250641 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/ConfigValidationTools.java @@ -0,0 +1,73 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import com.google.common.base.Strings; +import org.nzbhydra.config.RestartRequired; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class ConfigValidationTools { + + private static final Logger logger = LoggerFactory.getLogger(ConfigValidationTools.class); + + static void checkRegex(List errorMessages, String regex, String errorMessage) { + if (!Strings.isNullOrEmpty(regex)) { + try { + Pattern.compile(regex); + } catch (PatternSyntaxException e) { + errorMessages.add(errorMessage); + } + } + } + + /** + * Detects if any setting was changed that requires a restart to be effective + * + * @param oldConfig the old config (its settings will be compared with the ones from the calling instance) + * @return + */ + public static boolean isRestartNeeded(Object oldConfig, Object newConfig) { + for (Field field : oldConfig.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(RestartRequired.class)) { + try { + //PropertyDescriptor doesn't work for some reason, this is just as fine for what we need + String getterName = (field.getType() == Boolean.class || field.getType() == boolean.class ? "is" : "get") + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); + Method method = oldConfig.getClass().getDeclaredMethod(getterName); + Object oldValue = method.invoke(oldConfig); + Object newValue = method.invoke(newConfig); + + if (!Objects.equals(oldValue, newValue)) { + logger.debug("Restart needed because field {} has changed", field.getName()); + return true; + } + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + logger.error("Unable to determine if field '{}' in class {} was changed", field.getName(), oldConfig.getClass().getName()); + } + } + } + return false; + } +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/ConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/ConfigValidator.java new file mode 100644 index 000000000..a477f196f --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/ConfigValidator.java @@ -0,0 +1,55 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import org.nzbhydra.config.BaseConfig; + +public interface ConfigValidator { + + boolean doesValidate(Class clazz); + + /** + * @param oldBaseConfig old config state (e.g. to compare what has changed) + * @param newBaseConfig + * @param newConfig the new config. Will always be the same object as the one on which the method was called + * @return a list of error messages or an empty list when everything is fine + */ + ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, T newConfig); + + /** + * Called before the config is saved after the user made some changes. Use this to convert data, e.g. passwords. + * + * @param oldBaseConfig + */ + default T prepareForSaving(BaseConfig oldBaseConfig, T newConfig) { + return newConfig; + } + + /** + * Called before the config is transferred to the GUI. Use this to prepare data, e.g. passwords. + */ + default T updateAfterLoading(T newConfig) { + return newConfig; + } + + /** + * Called for a new config to initialize itself + */ + default T initializeNewConfig(T newConfig) { + return newConfig; + } +} diff --git a/core/src/main/java/org/nzbhydra/config/downloading/DownloaderConfig.java b/core/src/main/java/org/nzbhydra/config/validation/DownloaderConfigValidator.java similarity index 50% rename from core/src/main/java/org/nzbhydra/config/downloading/DownloaderConfig.java rename to core/src/main/java/org/nzbhydra/config/validation/DownloaderConfigValidator.java index b2adebc23..f50d74049 100644 --- a/core/src/main/java/org/nzbhydra/config/downloading/DownloaderConfig.java +++ b/core/src/main/java/org/nzbhydra/config/validation/DownloaderConfigValidator.java @@ -1,5 +1,5 @@ /* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,62 +14,33 @@ * limitations under the License. */ -package org.nzbhydra.config.downloading; +package org.nzbhydra.config.validation; -import com.google.common.base.Strings; -import lombok.Data; import org.nzbhydra.config.BaseConfig; -import org.nzbhydra.config.ValidatingConfig; +import org.nzbhydra.config.downloading.DownloaderConfig; +import org.nzbhydra.config.downloading.NzbAddingType; import org.nzbhydra.config.indexer.IndexerConfig; -import org.nzbhydra.config.sensitive.SensitiveData; -import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; -@Data -@ConfigurationProperties(prefix = "downloaders") -public class DownloaderConfig extends ValidatingConfig { - - @SensitiveData - private String apiKey; - private String defaultCategory; - private DownloadType downloadType; - private boolean enabled; - private String iconCssClass; - private String name; - private NzbAddingType nzbAddingType; - private DownloaderType downloaderType; - @SensitiveData - private String url; - @SensitiveData - private String username; - @SensitiveData - private String password; - private boolean addPaused; - - public DownloaderType getDownloaderType() { - return downloaderType; - } - - public Optional getUsername() { - return Optional.ofNullable(Strings.emptyToNull(username)); - } - - public Optional getPassword() { - return Optional.ofNullable(Strings.emptyToNull(password)); +@Component +public class DownloaderConfigValidator implements ConfigValidator { + @Override + public boolean doesValidate(Class clazz) { + return clazz == DownloaderConfig.class; } @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, DownloaderConfig newDownloaderConfig, BaseConfig newBaseConfig) { + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, DownloaderConfig newConfig) { List warnings = new ArrayList<>(); - if (isEnabledWithoutSendLink(newBaseConfig, "nzbs.in", newDownloaderConfig)) { + if (isEnabledWithoutSendLink(newBaseConfig, "nzbs.in", newConfig)) { warnings.add("nzbs.in forbids NZBHydra to download NZBs directly. The NZB adding type \"Send link\" will automatically used for this indexer."); } - if (isEnabledWithoutSendLink(newBaseConfig, "omgwtfnzbs", newDownloaderConfig)) { + if (isEnabledWithoutSendLink(newBaseConfig, "omgwtfnzbs", newConfig)) { warnings.add("omgwtfnzbs forbids NZBHydra to download NZBs directly. The NZB adding type \"Send link\" will automatically used for this indexer."); } return new ConfigValidationResult(true, false, Collections.emptyList(), warnings); @@ -78,21 +49,4 @@ public class DownloaderConfig extends ValidatingConfig { private static boolean isEnabledWithoutSendLink(BaseConfig newBaseConfig, String hostContains, DownloaderConfig newDownloaderConfig) { return newBaseConfig.getIndexers().stream().anyMatch(x -> x.getHost().toLowerCase().contains(hostContains) && x.getState() == IndexerConfig.State.ENABLED) && newDownloaderConfig.getNzbAddingType() != NzbAddingType.SEND_LINK; } - - @Override - public DownloaderConfig prepareForSaving(BaseConfig oldBaseConfig) { - return this; - } - - @Override - public DownloaderConfig updateAfterLoading() { - return this; - } - - @Override - public DownloaderConfig initializeNewConfig() { - return this; - } - - } diff --git a/core/src/main/java/org/nzbhydra/config/downloading/DownloadingConfig.java b/core/src/main/java/org/nzbhydra/config/validation/DownloadingConfigValidator.java similarity index 51% rename from core/src/main/java/org/nzbhydra/config/downloading/DownloadingConfig.java rename to core/src/main/java/org/nzbhydra/config/validation/DownloadingConfigValidator.java index 6960ff3f8..7d3f00cd5 100644 --- a/core/src/main/java/org/nzbhydra/config/downloading/DownloadingConfig.java +++ b/core/src/main/java/org/nzbhydra/config/validation/DownloadingConfigValidator.java @@ -1,5 +1,5 @@ /* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,55 +14,44 @@ * limitations under the License. */ -package org.nzbhydra.config.downloading; +package org.nzbhydra.config.validation; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.google.common.base.Strings; -import lombok.Data; -import org.javers.core.metamodel.annotation.DiffIgnore; import org.nzbhydra.config.BaseConfig; -import org.nzbhydra.config.SearchSourceRestriction; -import org.nzbhydra.config.ValidatingConfig; +import org.nzbhydra.config.downloading.DownloadingConfig; +import org.nzbhydra.config.downloading.FileDownloadAccessType; import org.nzbhydra.config.indexer.IndexerConfig; -import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -@Data -@ConfigurationProperties(prefix = "downloading") -public class DownloadingConfig extends ValidatingConfig { +@Component +public class DownloadingConfigValidator implements ConfigValidator { - @DiffIgnore - private List downloaders = new ArrayList<>(); - private String saveTorrentsTo; - private String saveNzbsTo; - private boolean sendMagnetLinks; - private boolean updateStatuses; - private boolean showDownloaderStatus = true; - @JsonFormat(shape = JsonFormat.Shape.STRING) - private FileDownloadAccessType nzbAccessType = FileDownloadAccessType.REDIRECT; - private SearchSourceRestriction fallbackForFailed = SearchSourceRestriction.BOTH; - private String externalUrl; - private String primaryDownloader; + @Autowired + private DownloaderConfigValidator downloaderConfigValidator; @Override - public ConfigValidationResult validateConfig(BaseConfig oldConfig, DownloadingConfig newConfig, BaseConfig newBaseConfig) { + public boolean doesValidate(Class clazz) { + return clazz == DownloadingConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, DownloadingConfig newConfig) { List errors = new ArrayList<>(); - if (getSaveTorrentsTo().isPresent()) { - File file = new File(getSaveTorrentsTo().get()); - validateBlackholeFolder(errors, file, getSaveTorrentsTo().get(), "Torrent"); + if (newConfig.getSaveTorrentsTo().isPresent()) { + File file = new File(newConfig.getSaveTorrentsTo().get()); + validateBlackholeFolder(errors, file, newConfig.getSaveTorrentsTo().get(), "Torrent"); } - if (getSaveNzbsTo().isPresent()) { - File file = new File(getSaveNzbsTo().get()); - validateBlackholeFolder(errors, file, getSaveNzbsTo().get(), "NZB"); + if (newConfig.getSaveNzbsTo().isPresent()) { + File file = new File(newConfig.getSaveNzbsTo().get()); + validateBlackholeFolder(errors, file, newConfig.getSaveNzbsTo().get(), "NZB"); } - List validationResults = downloaders.stream().map(downloaderConfig -> downloaderConfig.validateConfig(oldConfig, downloaderConfig, newBaseConfig)).collect(Collectors.toList()); - List downloaderErrors = validationResults.stream().map(ConfigValidationResult::getErrorMessages).flatMap(Collection::stream).collect(Collectors.toList()); + List validationResults = newConfig.getDownloaders().stream().map(downloaderConfig -> downloaderConfigValidator.validateConfig(oldBaseConfig, newBaseConfig, downloaderConfig)).toList(); + List downloaderErrors = validationResults.stream().map(ConfigValidationResult::getErrorMessages).flatMap(Collection::stream).toList(); errors.addAll(downloaderErrors); List warnings = new ArrayList<>(); @@ -74,7 +63,7 @@ public class DownloadingConfig extends ValidatingConfig { warnings.add("omgwftnzbs forbids NZBHydra to download NZBs directly. The NZB access type \"Redirect to indexer\" will automatically be used for this indexer."); } - warnings.addAll(validationResults.stream().map(ConfigValidationResult::getWarningMessages).flatMap(Collection::stream).collect(Collectors.toList())); + warnings.addAll(validationResults.stream().map(ConfigValidationResult::getWarningMessages).flatMap(Collection::stream).toList()); return new ConfigValidationResult(errors.isEmpty(), false, errors, warnings); } @@ -98,31 +87,4 @@ public class DownloadingConfig extends ValidatingConfig { } } - public Optional getSaveTorrentsTo() { - return Optional.ofNullable(Strings.emptyToNull(saveTorrentsTo)); - } - - public Optional getSaveNzbsTo() { - return Optional.ofNullable(saveNzbsTo); - } - - public Optional getExternalUrl() { - return Optional.ofNullable(externalUrl); - } - - @Override - public DownloadingConfig prepareForSaving(BaseConfig oldBaseConfig) { - return this; - } - - @Override - public DownloadingConfig updateAfterLoading() { - return this; - } - - @Override - public DownloadingConfig initializeNewConfig() { - return this; - } - } diff --git a/core/src/main/java/org/nzbhydra/config/validation/IndexerConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/IndexerConfigValidator.java new file mode 100644 index 000000000..494b01852 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/IndexerConfigValidator.java @@ -0,0 +1,79 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import com.google.common.base.Strings; +import org.apache.commons.lang3.StringUtils; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.searching.IndexerForSearchSelector; +import org.springframework.stereotype.Component; + +import java.time.format.DateTimeFormatter; +import java.util.regex.Matcher; + +@Component +public class IndexerConfigValidator implements ConfigValidator { + @Override + public boolean doesValidate(Class clazz) { + return clazz == IndexerConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, IndexerConfig newConfig) { + ConfigValidationResult validationResult = new ConfigValidationResult(); + + for (String schedule : newConfig.getSchedule()) { + Matcher matcher = IndexerForSearchSelector.SCHEDULER_PATTERN.matcher(schedule); + if (!matcher.matches()) { + validationResult.getErrorMessages().add("Indexer " + newConfig.getName() + " contains an invalid schedule: " + schedule); + } + } + if (newConfig.getHitLimit().isPresent() && newConfig.getHitLimit().get() <= 0) { + validationResult.getErrorMessages().add("Indexer " + newConfig.getName() + " has a hit limit of 0 or lower which doesn't make sense: "); + } + if (newConfig.getDownloadLimit().isPresent() && newConfig.getDownloadLimit().get() <= 0) { + validationResult.getErrorMessages().add("Indexer " + newConfig.getName() + " has a download limit of 0 or lower which doesn't make sense: "); + } + final String newExpirationDate = newConfig.getVipExpirationDate(); + if (newExpirationDate != null && !newExpirationDate.equals("Lifetime")) { + try { + DateTimeFormatter.ofPattern("yyyy-MM-dd").parse(newExpirationDate); + } catch (Exception e) { + validationResult.getErrorMessages().add("Invalid expiry date for indexer " + newConfig.getName() + ". Either use 'Lifetime' or use the format `YYYY-MM-DD"); + } + } + + newConfig.getCustomParameters().forEach(x -> { + if (Strings.isNullOrEmpty(x) || StringUtils.countMatches(x, '=') > 1) { + validationResult.getErrorMessages().add("The custom paramater " + x + " is invalid. You must use the format name=value."); + } + }); + + return validationResult; + } + + @Override + public IndexerConfig prepareForSaving(BaseConfig oldBaseConfig, IndexerConfig newConfig) { + if (newConfig.getState() == IndexerConfig.State.ENABLED || newConfig.getState() == IndexerConfig.State.DISABLED_USER) { + newConfig.setDisabledUntil(null); + newConfig.setDisabledLevel(0); + newConfig.setLastError(null); + } + return newConfig; + } +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/LoggingConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/LoggingConfigValidator.java new file mode 100644 index 000000000..5954786c0 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/LoggingConfigValidator.java @@ -0,0 +1,63 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.LoggingConfig; +import org.nzbhydra.logging.LoggingMarkers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Iterator; + +@Component +public class LoggingConfigValidator implements ConfigValidator { + + private static final Logger logger = LoggerFactory.getLogger(LoggingConfigValidator.class); + + @Override + public boolean doesValidate(Class clazz) { + return clazz == LoggingConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, LoggingConfig newConfig) { + ConfigValidationResult result = new ConfigValidationResult(); + + result.setRestartNeeded(ConfigValidationTools.isRestartNeeded(oldBaseConfig.getMain().getLogging(), newConfig)); + + if (newBaseConfig.getMain().getLogging().getMarkersToLog().size() > 3) { + result.getWarningMessages().add("You have more than 3 logging markers enabled. This is very rarely useful. Please make sure that this is actually needed. When creating debug infos please only enable those markers requested by the developer."); + } + + return result; + } + + @Override + public LoggingConfig prepareForSaving(BaseConfig oldBaseConfig, LoggingConfig newConfig) { + for (Iterator iterator = newConfig.getMarkersToLog().iterator(); iterator.hasNext(); ) { + String marker = iterator.next(); + if (Arrays.stream(LoggingMarkers.class.getDeclaredFields()).noneMatch(x -> x.getName().equals(marker))) { + logger.info("Removing logging marker that doesn't exist anymore."); + iterator.remove(); + } + } + return newConfig; + } +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/MainConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/MainConfigValidator.java new file mode 100644 index 000000000..2e3b7cfb7 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/MainConfigValidator.java @@ -0,0 +1,139 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import com.google.common.base.Strings; +import org.nzbhydra.NzbHydra; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.MainConfig; +import org.nzbhydra.debuginfos.DebugInfosProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.net.InetAddress; +import java.util.Random; + +import static org.nzbhydra.config.validation.ConfigValidationTools.isRestartNeeded; + +@Component +public class MainConfigValidator implements ConfigValidator { + + @Autowired + private LoggingConfigValidator loggingConfigValidator; + + @Override + public boolean doesValidate(Class clazz) { + return clazz == MainConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, MainConfig newConfig) { + ConfigValidationResult result = new ConfigValidationResult(); + MainConfig oldMain = oldBaseConfig.getMain(); + boolean portChanged = oldMain.getPort() != newConfig.getPort(); + boolean urlBaseChanged = oldMain.getUrlBase().isPresent() && !oldMain.getUrlBase().get().equals(newConfig.getUrlBase().orElse(null)); + if (newConfig.getUrlBase().isEmpty() && oldMain.getUrlBase().isPresent() && oldMain.getUrlBase().get().equals("/")) { + urlBaseChanged = false; + } + boolean sslChanged = oldMain.isSsl() != newConfig.isSsl(); + if (portChanged || urlBaseChanged || sslChanged && !newConfig.isStartupBrowser()) { + result.getWarningMessages().add("You've made changes that affect Hydra's URL and require a restart. Hydra will try and reload using the new URL when it's back."); + } + if (DebugInfosProvider.isRunInDocker() && !"0.0.0.0".equals(newConfig.getHost())) { + result.getWarningMessages().add("You've changed the host but NZBHydra seems to be run in docker. It's recommended to use the host '0.0.0.0'."); + } + + if (!"0.0.0.0".equals(newConfig.getHost())) { + try { + boolean reachable = InetAddress.getByName(newConfig.getHost()).isReachable(1); + if (!reachable) { + result.getWarningMessages().add("The configured host address cannot be reached. Are you sure it is correct?"); + } + } catch (IOException e) { + //Ignore, user will have to know what he does + } + } + if (oldBaseConfig.getMain().getXmx() < 128) { + result.getErrorMessages().add("The JVM memory must be set to at least 128"); + } + + MainConfig newMain = newBaseConfig.getMain(); + if (newMain.getKeepHistoryForWeeks() != null && newMain.getKeepHistoryForWeeks() <= 0) { + result.getErrorMessages().add("Please either delete the value for \"Keep history for\" or set it to a positive value."); + } + if (newMain.getKeepStatsForWeeks() != null && newMain.getKeepStatsForWeeks() <= 0) { + result.getErrorMessages().add("Please either delete the value for \"Keep stats for\" or set it to a positive value."); + } + if (newMain.getKeepStatsForWeeks() != null && newMain.getKeepHistoryForWeeks() != null && newMain.getKeepStatsForWeeks() > newMain.getKeepHistoryForWeeks()) { + result.getErrorMessages().add("Please set the time to keep stats to a value not higher than the time to keep history."); + } + + if (newMain.getBackupFolder() != null) { + final File backupFolderFile; + if (newConfig.getBackupFolder().contains(File.separator)) { + backupFolderFile = new File(newConfig.getBackupFolder()); + } else { + backupFolderFile = new File(NzbHydra.getDataFolder(), newConfig.getBackupFolder()); + } + if (!backupFolderFile.exists()) { + final boolean created = backupFolderFile.mkdirs(); + if (!created) { + result.getErrorMessages().add("Backup folder " + newConfig.getBackupFolder() + " does not exist and could not be created"); + } + } + } + + + ConfigValidationResult validationResult = loggingConfigValidator.validateConfig(oldBaseConfig, newBaseConfig, newConfig.getLogging()); + result.getWarningMessages().addAll(validationResult.getWarningMessages()); + result.getErrorMessages().addAll(validationResult.getErrorMessages()); + + oldMain = prepareForSaving(oldBaseConfig, oldMain); + result.setRestartNeeded(validationResult.isRestartNeeded() || isRestartNeeded(oldMain, newConfig)); + result.setOk(validationResult.isOk() && result.isOk()); + + return result; + } + + @Override + public MainConfig prepareForSaving(BaseConfig oldBaseConfig, MainConfig newConfig) { + final String urlBase = newConfig.getUrlBase().orElse(null); + if (!Strings.isNullOrEmpty(urlBase) && (!urlBase.startsWith("/") || urlBase.endsWith("/") || "/".equals(urlBase))) { + if (!urlBase.startsWith("/")) { + newConfig.setUrlBase("/" + urlBase); + } + if (urlBase.endsWith("/")) { + newConfig.setUrlBase(urlBase.substring(0, urlBase.length() - 1)); + } + if ("/".equals(urlBase) || "".equals(urlBase)) { + newConfig.setUrlBase("/"); + } + newConfig.setUrlBase(urlBase); + } + return newConfig; + } + + @Override + public MainConfig initializeNewConfig(MainConfig newConfig) { + Random random = new Random(); + newConfig.setApiKey(new BigInteger(130, random).toString(32).toUpperCase()); + return newConfig; + } +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/NotificationConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/NotificationConfigValidator.java new file mode 100644 index 000000000..29c2789f6 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/NotificationConfigValidator.java @@ -0,0 +1,52 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import joptsimple.internal.Strings; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.NotificationConfig; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class NotificationConfigValidator implements ConfigValidator { + @Override + public boolean doesValidate(Class clazz) { + return clazz == NotificationConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, NotificationConfig newConfig) { + final List errors = new ArrayList<>(); + final List warnings = new ArrayList<>(); + if (newConfig.getEntries().stream() + .anyMatch(x -> Strings.isNullOrEmpty(x.getAppriseUrls()))) { + errors.add("Make sure all notification entries contain a URL"); + } + + final boolean appriseUrlSet = !Strings.isNullOrEmpty(newConfig.getAppriseApiUrl()); + final boolean anyEntries = newConfig.getEntries().isEmpty(); + + if (anyEntries && !appriseUrlSet) { + warnings.add("No notifications will be sent unless the Apprise API URL is configured."); + } + + return new ConfigValidationResult(true, false, errors, warnings); + } +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/SearchingConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/SearchingConfigValidator.java new file mode 100644 index 000000000..d986d9e43 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/SearchingConfigValidator.java @@ -0,0 +1,102 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import com.google.common.base.Strings; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.SearchSourceRestriction; +import org.nzbhydra.config.SearchingConfig; +import org.nzbhydra.config.searching.CustomQueryAndTitleMapping; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; +import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.nzbhydra.config.validation.ConfigValidationTools.checkRegex; + +@Component +public class SearchingConfigValidator implements ConfigValidator { + + private static final Logger logger = LoggerFactory.getLogger(SearchingConfigValidator.class); + + @Override + public boolean doesValidate(Class clazz) { + return clazz == SearchingConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, SearchingConfig newConfig) { + List errors = new ArrayList<>(); + List warnings = new ArrayList<>(); + checkRegex(errors, newConfig.getRequiredRegex().orElse(null), "The required regex in \"Searching\" is invalid"); + checkRegex(errors, newConfig.getForbiddenRegex().orElse(null), "The forbidden in \"Searching\" is invalid"); + + if (newConfig.getApplyRestrictions() == SearchSourceRestriction.NONE) { + if (!newConfig.getRequiredWords().isEmpty() || !newConfig.getForbiddenWords().isEmpty()) { + warnings.add("You selected not to apply any word restrictions in \"Searching\" but supplied forbidden or required words there"); + } + if (newConfig.getRequiredRegex().isPresent() || newConfig.getForbiddenRegex().isPresent()) { + warnings.add("You selected not to apply any word restrictions in \"Searching\" but supplied a forbidden or required regex there"); + } + } + final CustomQueryAndTitleMappingHandler customQueryAndTitleMappingHandler = new CustomQueryAndTitleMappingHandler(newBaseConfig); + final SearchRequest searchRequest = new SearchRequest(); + searchRequest.setTitle("test title"); + searchRequest.setQuery("test query"); + for (CustomQueryAndTitleMapping customCustomQueryAndTitleMapping : newConfig.getCustomMappings()) { + try { + customQueryAndTitleMappingHandler.mapSearchRequest(searchRequest, Collections.singletonList(customCustomQueryAndTitleMapping)); + } catch (Exception e) { + errors.add(String.format("Unable to process mapping %s:}\n%s", customCustomQueryAndTitleMapping.toString(), e.getMessage())); + } + if (customCustomQueryAndTitleMapping.getFrom().contains("{episode:")) { + errors.add("The group 'episode' is not allowed in custom mapping input patterns."); + } + if (customCustomQueryAndTitleMapping.getFrom().contains("{season:")) { + errors.add("The group 'season' is not allowed in custom mapping input patterns."); + } + } + final List emptyTrailing = (newConfig.getRemoveTrailing().stream().filter(Strings::isNullOrEmpty)).toList(); + if (!emptyTrailing.isEmpty()) { + errors.add("Trailing values to remove contains empty values"); + } + + return new ConfigValidationResult(errors.isEmpty(), false, errors, warnings); + } + + @Override + public SearchingConfig prepareForSaving(BaseConfig oldBaseConfig, SearchingConfig newConfig) { + final Set customQuickfilterNames = newConfig.getCustomQuickFilterButtons().stream().map(x -> x.split("=")[0]).collect(Collectors.toSet()); + for (Iterator iterator = newConfig.getPreselectQuickFilterButtons().iterator(); iterator.hasNext(); ) { + String preselectQuickFilterButton = iterator.next(); + final String[] split = preselectQuickFilterButton.split("\\|"); + if ("custom".equals(split[0]) && !customQuickfilterNames.contains(split[0])) { + logger.info("Custom quickfilter {} doesn't exist anymore, removing it from list of filters to preselect.", preselectQuickFilterButton); + iterator.remove(); + } + } + return newConfig; + } +} diff --git a/core/src/main/java/org/nzbhydra/config/validation/UserAuthConfigValidator.java b/core/src/main/java/org/nzbhydra/config/validation/UserAuthConfigValidator.java new file mode 100644 index 000000000..23e4c3b5a --- /dev/null +++ b/core/src/main/java/org/nzbhydra/config/validation/UserAuthConfigValidator.java @@ -0,0 +1,51 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.config.validation; + +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.auth.UserAuthConfig; +import org.springframework.stereotype.Component; + +@Component +public class UserAuthConfigValidator implements ConfigValidator { + + @Override + public boolean doesValidate(Class clazz) { + return clazz == UserAuthConfig.class; + } + + @Override + public ConfigValidationResult validateConfig(BaseConfig oldBaseConfig, BaseConfig newBaseConfig, UserAuthConfig newConfig) { + return new ConfigValidationResult(); + } + + @Override + public UserAuthConfig prepareForSaving(BaseConfig oldBaseConfig, UserAuthConfig newConfig) { + if (newConfig.getPassword() != null && !newConfig.getPassword().startsWith(UserAuthConfig.PASSWORD_ID)) { + newConfig.setPassword(UserAuthConfig.PASSWORD_ID + newConfig.getPassword()); + } + return newConfig; + } + + @Override + public UserAuthConfig updateAfterLoading(UserAuthConfig newConfig) { + if (newConfig.getPassword() != null && newConfig.getPassword().startsWith(UserAuthConfig.PASSWORD_ID)) { + newConfig.setPassword(newConfig.getPassword().substring(6)); + } + return newConfig; + } +} diff --git a/core/src/main/java/org/nzbhydra/database/DatabaseRecreation.java b/core/src/main/java/org/nzbhydra/database/DatabaseRecreation.java index 012a7f651..292317390 100644 --- a/core/src/main/java/org/nzbhydra/database/DatabaseRecreation.java +++ b/core/src/main/java/org/nzbhydra/database/DatabaseRecreation.java @@ -16,179 +16,223 @@ package org.nzbhydra.database; -import com.google.common.collect.Sets; +import com.google.common.base.Joiner; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import org.apache.commons.io.IOUtils; -import org.flywaydb.core.internal.resource.StringResource; +import org.apache.commons.io.filefilter.WildcardFileFilter; +import org.flywaydb.core.Flyway; import org.nzbhydra.NzbHydra; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import java.io.File; -import java.io.FileWriter; +import java.io.FileReader; +import java.io.FilenameFilter; import java.io.IOException; -import java.nio.charset.Charset; +import java.io.InputStream; +import java.net.URI; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.sql.Connection; import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.HashSet; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.Set; +@SuppressWarnings("ResultOfMethodCallIgnored") public class DatabaseRecreation { private static final Logger logger = LoggerFactory.getLogger(DatabaseRecreation.class); private static final Map SCHEMA_VERSION_CHANGES = new LinkedHashMap<>(); - static { - SCHEMA_VERSION_CHANGES.put("'V1.11__REMOVE_INDEXERSTATUSES.sql', \\-?\\d+", "'V1.11__REMOVE_INDEXERSTATUSES.sql', 898390205"); - SCHEMA_VERSION_CHANGES.put("'V1.17__SHORTACCESS_AGAIN.sql', \\-?\\d+", "'V1.17__SHORTACCESS_AGAIN.sql', 177884391"); - } - public static void runDatabaseScript() throws ClassNotFoundException, SQLException { - if (!Thread.currentThread().getName().equals("main")) { - //During development this class is called twice (because of the Spring developer tools) - logger.debug("Skipping database script check for thread {}", Thread.currentThread().getName()); - return; - } + public static void runDatabaseScript() throws Exception { File databaseFile = new File(NzbHydra.getDataFolder(), "database/nzbhydra.mv.db"); File databaseScriptFile = new File(NzbHydra.getDataFolder(), "databaseScript.sql"); File databaseScriptFileNew = new File(NzbHydra.getDataFolder(), "databaseScriptNew.sql"); + File restoreScriptFile = new File(NzbHydra.getDataFolder(), "database/script.sql"); + String dbConnectionUrl = "jdbc:h2:file:" + databaseFile.getAbsolutePath().replace(".mv.db", ""); + Class.forName("org.h2.Driver"); + migrateToH2v2IfNeeded(databaseFile, dbConnectionUrl); + if (restoreScriptFile.exists() && !databaseScriptFile.exists()) { + DatabaseRecreation.logger.info("No database file found but script.sql - restoring database"); + try (Connection connection = DriverManager.getConnection(dbConnectionUrl, "sa", "sa")) { + connection.createStatement().executeUpdate("runscript from '%s';".formatted(restoreScriptFile.getCanonicalPath().replace("\\", "/"))); + restoreScriptFile.delete(); + } + } + + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private static void migrateToH2v2IfNeeded(File databaseFile, String dbConnectionUrl) throws Exception { if (!databaseFile.exists()) { logger.debug("No database file found - no recreation needed"); return; } - - Class.forName("org.h2.Driver"); - String dbConnectionUrl = "jdbc:h2:file:" + databaseFile.getAbsolutePath().replace(".mv.db", ""); - - if (isDatabaseRecreationNotNeeded(dbConnectionUrl)) { - return; - } - - createDatabaseScript(databaseScriptFile, dbConnectionUrl); - - deleteExistingDatabase(databaseFile); - - replaceSchemaVersionChanges(databaseScriptFile, databaseScriptFileNew); - - runDatabaseScript(databaseScriptFile, dbConnectionUrl); - } - - private static boolean isDatabaseRecreationNotNeeded(String dbConnectionUrl) throws SQLException { - logger.debug("Determining if database recreation is needed"); - Set executedScripts = new HashSet<>(); - try (Connection conn = DriverManager.getConnection(dbConnectionUrl, "SA", "")) { - ResultSet resultSet = conn.createStatement().executeQuery("select \"script\", \"checksum\" from \"schema_version\";"); - while (resultSet.next()) { - executedScripts.add(new ExecutedScript(resultSet.getString(1), resultSet.getInt((2)))); - } - } - PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); try { - HashSet resources = Sets.newHashSet(resolver.getResources("classpath:/migration/*")); - if (resources.stream().allMatch(x -> { - try { - StringResource stringResource = new StringResource(IOUtils.toString(x.getInputStream(), Charset.defaultCharset())); - ExecutedScript executedScript = new ExecutedScript(x.getFilename(), stringResource.checksum()); - boolean scriptExecuted = executedScripts.contains(executedScript); - if (!scriptExecuted) { - logger.info("Database recreation needed because {} was not yet executed or its checksum has changed", executedScript); - } - return scriptExecuted; - } catch (IOException e) { - throw new RuntimeException("Unable to determine checksum for " + x.getFilename()); - } - })) { - logger.debug("No migration scripts found to run. Skipping database recreation"); - return true; + char[] buffer; + try (FileReader fileReader = new FileReader(databaseFile)) { + buffer = new char[1024]; + final int read = fileReader.read(buffer); } - } catch (IOException e) { - throw new RuntimeException("Unable to find migration scripts", e); + final String header = new String(buffer).trim(); + if (header.contains("format:1")) { + logger.info("Determined existing database to be version 1.4. Migration needed."); + } else if (header.contains("format:2")) { + logger.info("Determined existing database to be version 2. No migration needed."); + return; + } else { + logger.error("Unable to determine database version from header {}", header); + throw new RuntimeException("Invalid database file header"); + } + } catch (Exception e) { + throw new RuntimeException("Unable to open database file " + databaseFile, e); } - return false; - } + boolean isUpgrade14To210 = true; + if (isUpgrade14To210) { - private static void runDatabaseScript(File databaseScriptFile, String dbConnectionUrl) throws SQLException { - try (Connection conn = DriverManager.getConnection(dbConnectionUrl, "SA", "")) { - logger.info("Running database script {} for reimport of old database", databaseScriptFile.getAbsolutePath()); try { - conn.createStatement().execute("runscript from '" + databaseScriptFile.getAbsolutePath() + "'"); - } catch (SQLException e) { - throw new RuntimeException("Unable to import database script", e); + final File[] traceFiles = databaseFile.getParentFile().listFiles((FilenameFilter) new WildcardFileFilter("*.trace.db")); + if (traceFiles != null) { + for (File traceFile : traceFiles) { + traceFile.delete(); + } + } + } catch (Exception e) { + logger.error("Unable to delete trace files", e); + } + + File backupDatabaseFile = null; + String javaExecutable; + final File h2OldJar; + final File h2NewJar; + final String scriptFilePath; + try { + javaExecutable = getJavaExecutable(); + h2OldJar = downloadJarFile("https://repo1.maven.org/maven2/com/h2database/h2/1.4.200/h2-1.4.200.jar"); + h2NewJar = downloadJarFile("https://repo1.maven.org/maven2/com/h2database/h2/2.1.214/h2-2.1.214.jar"); + } catch (Exception e) { + logger.error("Error migrating old database. Unable to download h2 jars"); + throw e; + } + try { + final File scriptFile = Files.createTempFile("nzbhydra", ".sql").toFile(); + scriptFile.deleteOnExit(); + scriptFilePath = scriptFile.getCanonicalPath(); + + logger.info("Running database migration from 1.4 to 2"); + + backupDatabaseFile = new File(databaseFile.getParent(), databaseFile.getName() + ".old.bak." + System.currentTimeMillis()); + logger.info("Copying old database file {} to backup {} which will be automatically deleted after 14 days", databaseFile, backupDatabaseFile); + Files.copy(databaseFile.toPath(), backupDatabaseFile.toPath()); + + final String updatePasswordQuery = "alter user sa set password 'sa'"; + updatePassword(dbConnectionUrl, javaExecutable, h2OldJar, updatePasswordQuery); + + runH2Command(Arrays.asList(javaExecutable, "-cp", h2OldJar.toString(), "org.h2.tools.Script", "-url", dbConnectionUrl, "-user", "sa", "-password", "sa", "-script", scriptFilePath), "Database export failed."); + } catch (Exception e) { + logger.error("Error migrating old database file to new one"); + if (backupDatabaseFile != null && backupDatabaseFile.exists()) { + if (backupDatabaseFile != null && backupDatabaseFile.exists()) { + backupDatabaseFile.delete(); + } + } + + throw e; + } + try { + final boolean deleted = databaseFile.delete(); + if (!deleted) { + throw new RuntimeException("Unable to delete old database file " + databaseFile); + } + + runH2Command(Arrays.asList(javaExecutable, "-cp", h2NewJar.toString(), "org.h2.tools.RunScript", "-url", dbConnectionUrl, "-user", "sa", "-password", "sa", "-script", scriptFilePath, "-options", "FROM_1X"), "Database import failed."); + + final Flyway flyway = Flyway.configure() + .dataSource(dbConnectionUrl, "sa", "sa") + .baselineDescription("INITIAL") + .baselineVersion("1") + .load(); + flyway.baseline(); + } catch (Exception e) { + logger.error("Error while trying to migrate database to 2.0"); + if (backupDatabaseFile != null && backupDatabaseFile.exists()) { + logger.info("Restoring database file {} from backup {}", databaseFile, backupDatabaseFile); + Files.move(backupDatabaseFile.toPath(), databaseFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + throw new RuntimeException(e); } - logger.info("Successfully recreated database"); - } - boolean deleted = databaseScriptFile.delete(); - if (!deleted) { - throw new RuntimeException("Unable to delete database script file at " + databaseScriptFile.getAbsolutePath() + ". Please delete it manually."); } } - private static void replaceSchemaVersionChanges(File databaseScriptFile, File databaseScriptFileNew) { + private static void updatePassword(String dbConnectionUrl, String javaExecutable, File h2OldJar, String updatePasswordQuery) throws IOException, InterruptedException { try { - try (FileWriter fileWriter = new FileWriter(databaseScriptFileNew)) { - Files.lines(databaseScriptFile.toPath()).forEach(line -> { - String sql = line; - for (Map.Entry entry : SCHEMA_VERSION_CHANGES.entrySet()) { - sql = sql.replaceAll(entry.getKey(), entry.getValue()); - } - try { - fileWriter.write(sql); - fileWriter.write(System.getProperty("line.separator")); - } catch (IOException e) { - throw new RuntimeException("Unable to write to temp file " + databaseScriptFileNew, e); - } - }); - } - if (!databaseScriptFile.delete()) { - throw new RuntimeException("Unable to delete existing database script file " + databaseScriptFile); - } - Files.move(databaseScriptFileNew.toPath(), databaseScriptFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - throw new RuntimeException("Unable to update database migration versions", e); + runH2Command(Arrays.asList(javaExecutable, "-cp", h2OldJar.toString(), "org.h2.tools.Shell", "-url", dbConnectionUrl, "-user", "sa", "-sql", NzbHydra.isOsWindows() ? ("\"" + updatePasswordQuery + "\"") : updatePasswordQuery), "Password update failed."); + } catch (Exception e) { + runH2Command(Arrays.asList(javaExecutable, "-cp", h2OldJar.toString(), "org.h2.tools.Shell", "-url", dbConnectionUrl, "-user", "sa", "-password", "sa", "-sql", NzbHydra.isOsWindows() ? ("\"" + updatePasswordQuery + "\"") : updatePasswordQuery), "Password update failed."); } } - private static void deleteExistingDatabase(File databaseFile) { - if (!databaseFile.exists()) { - throw new RuntimeException("Unable to find database file at " + databaseFile.getAbsolutePath()); - } - boolean deleted = databaseFile.delete(); - if (!deleted) { - throw new RuntimeException("Unable to delete database file at " + databaseFile.getAbsolutePath() + ". Please move it somewhere else (just to be sure) and restart NZBHYdra."); + private static void runH2Command(List updatePassCommand, String errorMessage) throws IOException, InterruptedException { + logger.info("Running command: " + Joiner.on(" ").join(updatePassCommand)); + final Process process = new ProcessBuilder(updatePassCommand) + .redirectErrorStream(true) + .inheritIO() + .start(); + final int result = process.waitFor(); + if (result != 0) { + throw new RuntimeException(errorMessage + ". Code: " + result); } } - private static void createDatabaseScript(File databaseScriptFile, String url) throws SQLException { - if (databaseScriptFile.exists()) { - boolean deleted = databaseScriptFile.delete(); - if (!deleted) { - throw new RuntimeException("Unable to delete database script file at " + databaseScriptFile.getAbsolutePath() + ". Please delete it manually and restart NZBHYdra."); + private static File downloadJarFile(String url) throws IOException { + final ClientHttpRequest request = new OkHttp3ClientHttpRequestFactory().createRequest(URI.create(url), HttpMethod.GET); + final File jarFile; + try (ClientHttpResponse response = request.execute()) { + jarFile = Files.createTempFile("nzbhydra", ".jar").toFile(); + logger.debug("Downloaded file from {} to {}. Will be deleted on exit", url, jarFile); + jarFile.deleteOnExit(); + try (InputStream body = response.getBody()) { + com.google.common.io.Files.asByteSink(jarFile).writeFrom(body); + } + if (response.getStatusCode() != HttpStatus.OK) { + throw new RuntimeException("Unable to download database library. Response: " + response.getStatusCode()); } } - logger.info("Recreating database to ensure successful migration. This may take a couple of minutes..."); + return jarFile; - try (Connection conn = DriverManager.getConnection(url, "SA", "")) { - logger.info("Creating database script {} from database", databaseScriptFile.getAbsolutePath()); - conn.createStatement().execute(String.format("script to '%s'", databaseScriptFile)); - if (!databaseScriptFile.exists()) { - throw new RuntimeException("Database script file was not created at " + databaseScriptFile.getAbsolutePath()); - } - } } + private static String getJavaExecutable() { + String javaExecutable; + if (System.getProperty("os.name").startsWith("Win")) { + javaExecutable = System.getProperties().getProperty("java.home") + File.separator + "bin" + File.separator + "java.exe"; + } else { + javaExecutable = System.getProperties().getProperty("java.home") + File.separator + "bin" + File.separator + "java"; + } + if (new File(javaExecutable).exists()) { + logger.debug("Determined java executable: {}", javaExecutable); + } else { + logger.debug("Java executable not found. Trying just java and hope it's in path"); + javaExecutable = "java"; + } + return javaExecutable; + } + + @Data + @ReflectionMarker @AllArgsConstructor @EqualsAndHashCode private static class ExecutedScript { diff --git a/core/src/main/java/org/nzbhydra/database/DatabaseRecreationBean.java b/core/src/main/java/org/nzbhydra/database/DatabaseRecreationBean.java new file mode 100644 index 000000000..faed65c0d --- /dev/null +++ b/core/src/main/java/org/nzbhydra/database/DatabaseRecreationBean.java @@ -0,0 +1,33 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.database; + + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.Ordered; + +public class DatabaseRecreationBean implements InitializingBean, Ordered { + @Override + public void afterPropertiesSet() throws Exception { + DatabaseRecreation.runDatabaseScript(); + } + + @Override + public int getOrder() { + return -100; + } +} diff --git a/core/src/main/java/org/nzbhydra/database/DatabaseRecreationConfig.java b/core/src/main/java/org/nzbhydra/database/DatabaseRecreationConfig.java new file mode 100644 index 000000000..277085efb --- /dev/null +++ b/core/src/main/java/org/nzbhydra/database/DatabaseRecreationConfig.java @@ -0,0 +1,43 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.database; + +import com.google.common.collect.Sets; +import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDatabaseInitializerDetector; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Set; + +@Configuration +public class DatabaseRecreationConfig extends AbstractBeansOfTypeDatabaseInitializerDetector { + + @Bean + public DatabaseRecreationBean getDatabaseRecreationBean() { + return new DatabaseRecreationBean(); + } + + @Override + protected Set> getDatabaseInitializerBeanTypes() { + return Sets.newHashSet(DatabaseRecreationBean.class); + } + + @Override + public int getOrder() { + return -1000; + } +} diff --git a/core/src/main/java/org/nzbhydra/database/FlywayMigration.java b/core/src/main/java/org/nzbhydra/database/FlywayMigration.java deleted file mode 100644 index 9fa0276fe..000000000 --- a/core/src/main/java/org/nzbhydra/database/FlywayMigration.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.database; - -import org.flywaydb.core.Flyway; -import org.flywaydb.core.api.FlywayException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.sql.SQLException; - -@Configuration -public class FlywayMigration { - - private static final Logger logger = LoggerFactory.getLogger(FlywayMigration.class); - - - @Bean - public FlywayMigrationStrategy flywayMigrationStrategy() { - return new FlywayMigrationStrategy() { - @Override - public void migrate(Flyway flyway) { - try { - flyway.migrate(); - } catch (FlywayException e) { - if (e.getMessage().contains("1.15")) { - logger.info("Found failed database migration. Attempting repair"); - flyway.repair(); - try { - flyway.getConfiguration().getDataSource().getConnection().createStatement().executeUpdate("delete from PUBLIC.\"schema_version\" where \"version\" = '1.15' or \"version\" = '1.16'"); - } catch (SQLException e1) { - logger.error("Error while deleting old migration steps", e); - } - flyway.migrate(); - } else if (e.getMessage().contains("1.21")) { - logger.info("Found failed database migration. Attempting repair"); - flyway.repair(); - try { - flyway.getConfiguration().getDataSource().getConnection().createStatement().execute("delete from PUBLIC.\"schema_version\" where \"version\" = '1.15' or \"version\" = '1.16'"); - flyway.getConfiguration().getDataSource().getConnection().createStatement().executeUpdate("delete from PUBLIC.\"schema_version\" where \"version\" = '1.15' or \"version\" = '1.16'"); - } catch (SQLException e1) { - logger.error("Error while deleting old migration steps", e); - } - flyway.migrate(); - } else if (e.getMessage().contains("Applied to database : 182559665")) { - //Script had to be changed because with update to SB 2.2 and Flyway 6.0 a comment using // was not properly read in the file V1.0__INITIAL.sql - logger.debug("Reparing changed initial migration SQL checksum"); -// flyway.repair(); - try { - flyway.getConfiguration().getDataSource().getConnection().createStatement().execute("update \"schema_version\" x set x.\"checksum\" = 1776042577 where x.\"script\" = 'V1.0__INITIAL.sql'"); - } catch (SQLException e1) { - logger.error("Error while changing initial migration SQL checksum", e); - } - flyway.migrate(); - } else { - throw new RuntimeException("Error while migrating database", e); - } - } - } - }; - } -} diff --git a/core/src/main/java/org/nzbhydra/database/H2DialectExtended.java b/core/src/main/java/org/nzbhydra/database/H2DialectExtended.java new file mode 100644 index 000000000..bdd8ce6cd --- /dev/null +++ b/core/src/main/java/org/nzbhydra/database/H2DialectExtended.java @@ -0,0 +1,69 @@ +/* + * (C) Copyright 2022 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.database; + +import org.h2.engine.Constants; +import org.hibernate.MappingException; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.SimpleDatabaseVersion; +import org.hibernate.dialect.sequence.ANSISequenceSupport; +import org.hibernate.dialect.sequence.SequenceSupport; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; +import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; + +public class H2DialectExtended extends H2Dialect { + + public H2DialectExtended(DialectResolutionInfo info) { + this(); + } + + public H2DialectExtended() { + //Instance is created without DatabaseVersion info (or ZERO) + super(new SimpleDatabaseVersion(Constants.VERSION_MAJOR, Constants.VERSION_MINOR, Constants.BUILD_ID)); + } + + public H2DialectExtended(DatabaseVersion version) { + this(); + } + + @Override + public String toBooleanValueString(boolean bool) { + return bool ? "TRUE" : "FALSE"; + } + + @Override + public SequenceInformationExtractor getSequenceInformationExtractor() { + return SequenceInformationExtractorLegacyImpl.INSTANCE; + } + + @Override + public String getQuerySequencesString() { + return "select * from INFORMATION_SCHEMA.SEQUENCES"; + } + + @Override + public SequenceSupport getSequenceSupport() { + return new ANSISequenceSupport() { + @Override + public String getSequenceNextValString(String sequenceName) throws MappingException { + return "values next value for " + sequenceName; + } + }; + } +} diff --git a/core/src/main/java/org/nzbhydra/database/migration/V2__MOVE_GENERIC_STORAGE.java b/core/src/main/java/org/nzbhydra/database/migration/V2__MOVE_GENERIC_STORAGE.java deleted file mode 100644 index c6f747989..000000000 --- a/core/src/main/java/org/nzbhydra/database/migration/V2__MOVE_GENERIC_STORAGE.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.database.migration; - -import org.flywaydb.core.api.migration.BaseJavaMigration; -import org.flywaydb.core.api.migration.Context; -import org.nzbhydra.config.BaseConfig; -import org.nzbhydra.config.ConfigReaderWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.sql.ResultSet; -import java.sql.Statement; - -public class V2__MOVE_GENERIC_STORAGE extends BaseJavaMigration { - - private static final Logger logger = LoggerFactory.getLogger(V2__MOVE_GENERIC_STORAGE.class); - - @Override - public void migrate(Context context) throws Exception { - ConfigReaderWriter configReaderWriter = new ConfigReaderWriter(); - BaseConfig baseConfig = configReaderWriter.loadSavedConfig(); - try (Statement statement = context.getConnection().createStatement()) { - try (ResultSet resultSet = statement.executeQuery("select * from GENERIC_STORAGE_DATA")) { - while (resultSet.next()) { - String data = resultSet.getString(2); - String key = resultSet.getString(3); - baseConfig.getGenericStorage().put(key, data); - logger.debug("Migrating GenericStorageData with key {}", key); - } - } - } - configReaderWriter.save(baseConfig); - logger.info("Migrated {} GenericStorageData entries", baseConfig.getGenericStorage().size()); - } -} diff --git a/core/src/main/java/org/nzbhydra/database/migration/V3__MOVE_GENERIC_STORAGE.java b/core/src/main/java/org/nzbhydra/database/migration/V3__MOVE_GENERIC_STORAGE.java deleted file mode 100644 index 31363f93d..000000000 --- a/core/src/main/java/org/nzbhydra/database/migration/V3__MOVE_GENERIC_STORAGE.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.database.migration; - -import org.flywaydb.core.api.migration.BaseJavaMigration; -import org.flywaydb.core.api.migration.Context; -import org.nzbhydra.config.BaseConfig; -import org.nzbhydra.config.ConfigReaderWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.sql.ResultSet; -import java.sql.Statement; - -public class V3__MOVE_GENERIC_STORAGE extends BaseJavaMigration { - - private static final Logger logger = LoggerFactory.getLogger(V3__MOVE_GENERIC_STORAGE.class); - - @Override - public void migrate(Context context) throws Exception { - ConfigReaderWriter configReaderWriter = new ConfigReaderWriter(); - BaseConfig baseConfig = configReaderWriter.loadSavedConfig(); - try (Statement statement = context.getConnection().createStatement()) { - try (ResultSet resultSet = statement.executeQuery("select * from GENERIC_STORAGE_DATA")) { - while (resultSet.next()) { - String data = resultSet.getString(2); - String key = resultSet.getString(3); - baseConfig.getGenericStorage().put(key, data); - logger.debug("Migrating GenericStorageData with key {}", key); - } - } - } - configReaderWriter.save(baseConfig); - logger.info("Migrated {} GenericStorageData entries", baseConfig.getGenericStorage().size()); - } -} diff --git a/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosProvider.java b/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosProvider.java index 576e565d9..9af85e3ab 100644 --- a/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosProvider.java +++ b/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosProvider.java @@ -1,9 +1,14 @@ package org.nzbhydra.debuginfos; import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.Pair; import org.javers.core.JaversBuilder; import org.javers.core.diff.Diff; import org.nzbhydra.Jackson; @@ -17,6 +22,7 @@ import org.nzbhydra.logging.LogAnonymizer; import org.nzbhydra.logging.LogContentProvider; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.problemdetection.OutdatedWrapperDetector; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.update.UpdateManager; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; import org.nzbhydra.webaccess.Ssl; @@ -26,12 +32,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.management.ThreadDumpEndpoint; import org.springframework.boot.actuate.metrics.MetricsEndpoint; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import javax.annotation.PostConstruct; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; @@ -47,6 +51,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -82,6 +87,9 @@ public class DebugInfosProvider { private EntityManager entityManager; @Autowired private OutdatedWrapperDetector wrapperDetector; + + @Autowired + private ConfigurableEnvironment environment; @Autowired private Ssl ssl; @@ -170,6 +178,20 @@ public class DebugInfosProvider { return timeAndThreadCpuUsagesList; } + public static Pair getVersionAndBuildTimestamp() { + final Properties properties = new Properties(); + try { + properties.load(DebugInfosProvider.class.getResourceAsStream("/config/application.properties")); + } catch (Exception e) { + try { + properties.load(DebugInfosProvider.class.getResourceAsStream("/application.properties")); + } catch (Exception ex) { + throw new RuntimeException("Unable to load application properties", ex); + } + } + return Pair.of(properties.getProperty("build.version"), properties.getProperty("build.timestamp")); + } + private double getUpTimeInMiliseconds() { return metricsEndpoint.metric("process.uptime", null).getMeasurements().get(0).getValue() * 1000; } @@ -213,7 +235,7 @@ public class DebugInfosProvider { logger.info("Metrics:"); final Set metricsNames = metricsEndpoint.listNames().getNames(); for (String metric : metricsNames) { - final MetricsEndpoint.MetricResponse response = metricsEndpoint.metric(metric, null); + final MetricsEndpoint.MetricDescriptor response = metricsEndpoint.metric(metric, null); logger.info(metric + ": " + response.getMeasurements().stream() .map(x -> x.getStatistic().name() + ": " + formatSample(metric, x.getValue())) .collect(Collectors.joining(", "))); @@ -265,7 +287,12 @@ public class DebugInfosProvider { } public void logThreadDump() { - logger.debug(threadDumpEndpoint.textThreadDump()); + try { + //Fails on native image + logger.debug(threadDumpEndpoint.textThreadDump()); + } catch (Exception e) { + logger.error("Unable to create thread dump : {}", e.getMessage()); + } } private String formatSample(String name, Double value) { @@ -362,6 +389,7 @@ public class DebugInfosProvider { } @Data +@ReflectionMarker public static class TimeAndThreadCpuUsages { private final Instant time; private final List threadCpuUsages = new ArrayList<>(); @@ -372,16 +400,22 @@ public class DebugInfosProvider { } @Data +@ReflectionMarker @AllArgsConstructor public static class ThreadCpuUsage { private final String threadName; private final long cpuUsage; } + @EqualsAndHashCode(callSuper = true) @Data + @ReflectionMarker public static class DiffableCategoriesConfig extends CategoriesConfig { private Map categoriesMap = new HashMap<>(); + public DiffableCategoriesConfig() { + } + public DiffableCategoriesConfig(CategoriesConfig categoriesConfig) { categoriesConfig.getCategories().forEach(x -> { categoriesMap.put(x.getName(), x); diff --git a/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosWeb.java b/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosWeb.java index 1e7cdbd1a..749680452 100644 --- a/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosWeb.java +++ b/core/src/main/java/org/nzbhydra/debuginfos/DebugInfosWeb.java @@ -7,9 +7,11 @@ import lombok.AllArgsConstructor; import lombok.Data; import org.nzbhydra.GenericResponse; import org.nzbhydra.NzbHydra; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.logging.LogContentProvider; import org.nzbhydra.logging.LogContentProvider.JsonLogResponse; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -52,6 +54,8 @@ public class DebugInfosWeb { private MappingsEndpoint mappingsEndpoint; @Autowired private ConfigProvider configProvider; + @Autowired + private BaseConfigHandler baseConfigHandler; private static final Logger logger = LoggerFactory.getLogger(DebugInfosWeb.class); @@ -178,8 +182,8 @@ public class DebugInfosWeb { public ResponseEntity> getEndpoints() { final List conditionsDescriptions = ((Map>) mappingsEndpoint.mappings().getContexts().get("NZBHydra2").getMappings().get("dispatcherServlets")).get("dispatcherServlet") - .stream().filter(x1 -> x1.getHandler().contains("nzbhydra")) - .map(x -> x.getDetails().getRequestMappingConditions()).collect(Collectors.toList()); + .stream().filter(x1 -> x1.getHandler().contains("nzbhydra")) + .map(x -> x.getDetails().getRequestMappingConditions()).toList(); final List prefixAndEndpoints = new ArrayList<>(); final Multimap endpoints = HashMultimap.create(); @@ -212,12 +216,13 @@ public class DebugInfosWeb { final String msg = "Set log file level to debug and enabled the following logging markers: " + markersToEnable; logger.info(msg); - configProvider.getBaseConfig().save(true); + baseConfigHandler.save(true); return ResponseEntity.ok(msg); } @Data +@ReflectionMarker public static class ThreadCpuUsageChartData { private final String key; private final List values; @@ -234,6 +239,7 @@ public class DebugInfosWeb { } @Data +@ReflectionMarker @AllArgsConstructor public static class TimeAndValue { private final Instant time; @@ -241,6 +247,7 @@ public class DebugInfosWeb { } @Data +@ReflectionMarker @AllArgsConstructor public static class PrefixAndEndpoint { private final String prefix; @@ -249,6 +256,7 @@ public class DebugInfosWeb { } @Data +@ReflectionMarker @AllArgsConstructor public static class Endpoint { private final String endpoint; diff --git a/core/src/main/java/org/nzbhydra/downloading/DownloadResult.java b/core/src/main/java/org/nzbhydra/downloading/DownloadResult.java index e89db44fa..1e13de247 100644 --- a/core/src/main/java/org/nzbhydra/downloading/DownloadResult.java +++ b/core/src/main/java/org/nzbhydra/downloading/DownloadResult.java @@ -17,7 +17,8 @@ package org.nzbhydra.downloading; import lombok.Data; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; +import org.nzbhydra.config.downloading.DownloadType; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -30,6 +31,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; @Data +@ReflectionMarker public class DownloadResult { private static final Logger logger = LoggerFactory.getLogger(DownloadResult.class); @@ -65,7 +67,7 @@ public class DownloadResult { protected String getFileName() { String filename = title; - if (downloadEntity.getSearchResult().getDownloadType() == SearchResultItem.DownloadType.NZB) { + if (downloadEntity.getSearchResult().getDownloadType() == DownloadType.NZB) { filename += ".nzb"; } else { filename += ".torrent"; diff --git a/core/src/main/java/org/nzbhydra/downloading/FileDownloadEntity.java b/core/src/main/java/org/nzbhydra/downloading/FileDownloadEntity.java index 845d47373..7b81d515e 100644 --- a/core/src/main/java/org/nzbhydra/downloading/FileDownloadEntity.java +++ b/core/src/main/java/org/nzbhydra/downloading/FileDownloadEntity.java @@ -1,27 +1,43 @@ package org.nzbhydra.downloading; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.downloading.FileDownloadAccessType; import org.nzbhydra.searching.db.SearchResultEntity; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.web.SessionStorage; -import javax.persistence.*; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; @Data +@ReflectionMarker @Entity @Table(name = "indexernzbdownload", indexes = {@Index(name = "NZB_DOWNLOAD_EXT_ID", columnList = "EXTERNAL_ID")}) -public class FileDownloadEntity { +public final class FileDownloadEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) - protected int id; + @SequenceGenerator(allocationSize = 1, name = "INDEXERNZBDOWNLOAD_SEQ") + private int id; @ManyToOne + @JsonIgnoreProperties(value = {"handler", "hibernateLazyInitializer"}) @OnDelete(action = OnDeleteAction.CASCADE) private SearchResultEntity searchResult; @Enumerated(EnumType.STRING) diff --git a/core/src/main/java/org/nzbhydra/downloading/FileDownloadEvent.java b/core/src/main/java/org/nzbhydra/downloading/FileDownloadEvent.java index 3fba646d2..8548cc4b5 100644 --- a/core/src/main/java/org/nzbhydra/downloading/FileDownloadEvent.java +++ b/core/src/main/java/org/nzbhydra/downloading/FileDownloadEvent.java @@ -22,8 +22,6 @@ import org.nzbhydra.searching.db.SearchResultEntity; @Getter public class FileDownloadEvent { - private final long searchResultEntityId; - private final int downloadEntityId; private final FileDownloadEntity fileDownloadEntity; @@ -31,8 +29,6 @@ public class FileDownloadEvent { public FileDownloadEvent(FileDownloadEntity fileDownloadEntity, SearchResultEntity searchResultEntityHasDownloaded) { - this.downloadEntityId = fileDownloadEntity.getId(); - this.searchResultEntityId = searchResultEntityHasDownloaded.getId(); this.fileDownloadEntity = fileDownloadEntity; this.searchResultEntity = searchResultEntityHasDownloaded; } diff --git a/core/src/main/java/org/nzbhydra/downloading/FileHandler.java b/core/src/main/java/org/nzbhydra/downloading/FileHandler.java index 12cdde0b5..7fb104bb7 100644 --- a/core/src/main/java/org/nzbhydra/downloading/FileHandler.java +++ b/core/src/main/java/org/nzbhydra/downloading/FileHandler.java @@ -2,6 +2,7 @@ package org.nzbhydra.downloading; import com.google.common.base.Stopwatch; import com.google.common.collect.Sets; +import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; @@ -9,6 +10,8 @@ import org.jetbrains.annotations.NotNull; import org.nzbhydra.GenericResponse; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.MainConfig; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.downloading.FileDownloadAccessType; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.indexers.Indexer; @@ -21,8 +24,6 @@ import org.nzbhydra.notifications.DownloadNotificationEvent; import org.nzbhydra.searching.SearchModuleProvider; import org.nzbhydra.searching.db.SearchResultEntity; import org.nzbhydra.searching.db.SearchResultRepository; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.nzbhydra.web.UrlCalculator; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; import org.slf4j.Logger; @@ -103,7 +104,7 @@ public class FileHandler { @NotNull private SearchResultEntity getResultFromGuid(long guid, SearchSource accessSource) throws InvalidSearchResultIdException { Optional optionalResult = searchResultRepository.findById(guid); - if (!optionalResult.isPresent()) { + if (optionalResult.isEmpty()) { logger.error("Download request with invalid/outdated GUID {}", guid); throw new InvalidSearchResultIdException(guid, accessSource == SearchSource.INTERNAL); } @@ -126,7 +127,7 @@ public class FileHandler { if (downloadResult.isSuccessful()) { return downloadResult; } - if (!configProvider.getBaseConfig().getDownloading().getFallbackForFailed().meets(accessSource)) { + if (!accessSource.meets(configProvider.getBaseConfig().getDownloading().getFallbackForFailed())) { return downloadResult; } @@ -351,7 +352,7 @@ public class FileHandler { public NfoResult getNfo(Long searchResultId) { Optional optionalResult = searchResultRepository.findById(searchResultId); - if (!optionalResult.isPresent()) { + if (optionalResult.isEmpty()) { logger.error("Download request with invalid/outdated search result ID " + searchResultId); throw new RuntimeException("Download request with invalid/outdated search result ID " + searchResultId); } @@ -372,7 +373,8 @@ public class FileHandler { Request request = new Request.Builder().url(result.getLink()).build(); Indexer indexerByName = searchModuleProvider.getIndexerByName(result.getIndexer().getName()); Integer timeout = indexerByName.getConfig().getTimeout().orElse(configProvider.getBaseConfig().getSearching().getTimeout()); - try (Response response = clientHttpRequestFactory.getOkHttpClientBuilder(request.url().uri()).readTimeout(timeout, TimeUnit.SECONDS).connectTimeout(timeout, TimeUnit.SECONDS).followRedirects(true).build().newCall(request).execute()) { + final OkHttpClient build = clientHttpRequestFactory.getOkHttpClient(request.url().uri().getHost(), timeout); + try (Response response = build.newCall(request).execute()) { if (response.isRedirect()) { return handleRedirect(result, response); } @@ -406,7 +408,7 @@ public class FileHandler { } public GenericResponse saveNzbToBlackhole(Long searchResultId) { - if (!configProvider.getBaseConfig().getDownloading().getSaveNzbsTo().isPresent()) { + if (configProvider.getBaseConfig().getDownloading().getSaveNzbsTo().isEmpty()) { //Shouldn't happen return GenericResponse.notOk("NZBs black hole not set"); } diff --git a/core/src/main/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaver.java b/core/src/main/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaver.java index aad027bb9..39ff8902e 100644 --- a/core/src/main/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaver.java +++ b/core/src/main/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaver.java @@ -16,8 +16,7 @@ package org.nzbhydra.downloading; -import org.hibernate.Session; -import org.hibernate.SessionFactory; +import jakarta.annotation.PostConstruct; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.indexers.IndexerEntity; import org.nzbhydra.indexers.IndexerSearchEntity; @@ -31,9 +30,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; -import javax.persistence.EntityManagerFactory; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -51,10 +52,18 @@ public class IndexerUniquenessScoreSaver { private SearchResultRepository searchResultRepository; @Autowired private IndexerSearchRepository indexerSearchRepository; + @Autowired private IndexerUniquenessScoreEntityRepository indexerUniquenessScoreEntityRepository; @Autowired - private EntityManagerFactory entityManagerFactory; + private PlatformTransactionManager transactionManager; + private TransactionTemplate transactionTemplate; + + @PostConstruct + public void init() { + transactionTemplate = new TransactionTemplate(transactionManager); + } + @EventListener public void onNzbDownloadEvent(FileDownloadEvent downloadEvent) { @@ -63,26 +72,31 @@ public class IndexerUniquenessScoreSaver { return; } - handleDownloadEvent(downloadEvent); + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + handleDownloadEvent(downloadEvent); + } + }); + } - @Transactional - public void handleDownloadEvent(FileDownloadEvent downloadEvent) { - try (Session session = entityManagerFactory.unwrap(SessionFactory.class).openSession()) { - //For some reason the IndexerSearchEntity is not readable (LazyInitializationException) if the result is not loaded again - SearchResultEntity searchResultEntity = session.load(SearchResultEntity.class, downloadEvent.getSearchResultEntity().getId()); + private void handleDownloadEvent(FileDownloadEvent downloadEvent) { + try { + SearchResultEntity searchResultEntity = downloadEvent.getSearchResultEntity(); - if (searchResultEntity.getIndexerSearchEntity() == null) { + if (searchResultEntity.getIndexerSearchEntityId() == null) { logger.debug("Unable to determine indexer uniqueness score for result {} because no indexer search is saved", searchResultEntity.getTitle()); return; } - Set allIndexerSearchesInvolved = getIndexersInvolved(searchResultEntity); + final IndexerSearchEntity indexerSearchEntity = indexerSearchRepository.getReferenceById(searchResultEntity.getIndexerSearchEntityId()); + Set allIndexerSearchesInvolved = getIndexersInvolved(indexerSearchEntity); - Set indexersContainingSameResult = getIndexersFoundSameResult(searchResultEntity); + Set indexersContainingSameResult = getIndexersFoundSameResult(searchResultEntity, indexerSearchEntity); Set involvedIndexersWithoutResult = allIndexerSearchesInvolved.stream().filter(x -> !indexersContainingSameResult.contains(x.getIndexerEntity()) && !x.getIndexerEntity().equals(searchResultEntity.getIndexer())) - .collect(Collectors.toSet()); + .collect(Collectors.toSet()); saveScoresToDatabase(searchResultEntity.getIndexer(), indexersContainingSameResult, allIndexerSearchesInvolved, involvedIndexersWithoutResult); @@ -108,25 +122,24 @@ public class IndexerUniquenessScoreSaver { int involved = allIndexerSearchesInvolved.size(); int haveResult = indexersContainingSameResult.size() + 1; scoreEntities.add(new IndexerUniquenessScoreEntity(indexerDownloadedFrom, involved, haveResult, true)); - scoreEntities.addAll(indexersContainingSameResult.stream().map(x -> new IndexerUniquenessScoreEntity(x, involved, haveResult, true)).collect(Collectors.toList())); - scoreEntities.addAll(involvedIndexersWithoutResult.stream().map(x -> new IndexerUniquenessScoreEntity(x.getIndexerEntity(), involved, haveResult, false)).collect(Collectors.toList())); + scoreEntities.addAll(indexersContainingSameResult.stream().map(x -> new IndexerUniquenessScoreEntity(x, involved, haveResult, true)).toList()); + scoreEntities.addAll(involvedIndexersWithoutResult.stream().map(x -> new IndexerUniquenessScoreEntity(x.getIndexerEntity(), involved, haveResult, false)).toList()); indexerUniquenessScoreEntityRepository.saveAll(scoreEntities); } - private Set getIndexersInvolved(SearchResultEntity searchResultEntity) { - IndexerSearchEntity indexerSearchEntity = searchResultEntity.getIndexerSearchEntity(); + private Set getIndexersInvolved(IndexerSearchEntity indexerSearchEntity) { return new HashSet<>(indexerSearchRepository.findBySearchEntity(indexerSearchEntity.getSearchEntity())).stream().filter(IndexerSearchEntity::getSuccessful).collect(Collectors.toSet()); } - private Set getIndexersFoundSameResult(SearchResultEntity searchResultEntity) { + private Set getIndexersFoundSameResult(SearchResultEntity searchResultEntity, IndexerSearchEntity indexerSearchEntity) { Set resultsWithSameTitle = searchResultRepository.findAllByTitleLikeIgnoreCase(searchResultEntity.getTitle().replaceAll("[ .\\-_]", "_")); Set indexersContainingSameResult = new HashSet<>(); for (SearchResultEntity searchResult : resultsWithSameTitle) { if (searchResult.getIndexer().equals(searchResultEntity.getIndexer())) { continue; } - if (searchResult.getIndexerSearchEntity() != null && !searchResult.getIndexerSearchEntity().getSuccessful()) { + if (indexerSearchEntity != null && !indexerSearchEntity.getSuccessful()) { continue; } indexersContainingSameResult.add(searchResult.getIndexer()); diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/ConnectionCheckResult.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/ConnectionCheckResult.java index d99cd3256..b451a444d 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/ConnectionCheckResult.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/ConnectionCheckResult.java @@ -19,8 +19,10 @@ package org.nzbhydra.downloading.downloaders; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class ConnectionCheckResult { diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/Downloader.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/Downloader.java index f6b3da4f9..447e6d439 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/Downloader.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/Downloader.java @@ -20,10 +20,13 @@ import com.google.common.base.Joiner; import com.google.common.base.Stopwatch; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import jakarta.persistence.EntityNotFoundException; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; import org.nzbhydra.GenericResponse; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.downloading.DownloaderConfig; import org.nzbhydra.config.downloading.FileDownloadAccessType; import org.nzbhydra.config.downloading.NzbAddingType; @@ -41,16 +44,12 @@ import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.notifications.DownloadCompletionNotificationEvent; import org.nzbhydra.searching.db.SearchResultEntity; import org.nzbhydra.searching.db.SearchResultRepository; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import javax.persistence.EntityNotFoundException; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -78,20 +77,23 @@ public abstract class Downloader { } - @Autowired protected FileHandler nzbHandler; - @Autowired protected SearchResultRepository searchResultRepository; - @Autowired - private ApplicationEventPublisher applicationEventPublisher; - @Autowired - private IndexerSpecificDownloadExceptions indexerSpecificDownloadExceptions; - @Autowired - private ConfigProvider configProvider; + private final ApplicationEventPublisher applicationEventPublisher; + private final IndexerSpecificDownloadExceptions indexerSpecificDownloadExceptions; + protected final ConfigProvider configProvider; protected DownloaderConfig downloaderConfig; protected List downloadRates = new ArrayList<>(); + public Downloader(FileHandler nzbHandler, SearchResultRepository searchResultRepository, ApplicationEventPublisher applicationEventPublisher, IndexerSpecificDownloadExceptions indexerSpecificDownloadExceptions, ConfigProvider configProvider) { + this.nzbHandler = nzbHandler; + this.searchResultRepository = searchResultRepository; + this.applicationEventPublisher = applicationEventPublisher; + this.indexerSpecificDownloadExceptions = indexerSpecificDownloadExceptions; + this.configProvider = configProvider; + } + public void initialize(DownloaderConfig downloaderConfig) { this.downloaderConfig = downloaderConfig; } @@ -129,18 +131,18 @@ public abstract class Downloader { categoryToSend = category; } - SearchResultEntity searchResult = null; Optional optionalResult = searchResultRepository.findById(guid); - if (!optionalResult.isPresent()) { + if (optionalResult.isEmpty()) { logger.error("Download request with invalid/outdated GUID {}", guid); throw new InvalidSearchResultIdException(guid, true); } + final SearchResultEntity searchResult = optionalResult.get(); + final String searchResultTitle = optionalResult.get().getTitle(); final IndexerConfig indexerConfig = configProvider.getIndexerByName(optionalResult.get().getIndexer().getName()); try { final FileDownloadAccessType accessTypeForIndexer = indexerSpecificDownloadExceptions.getAccessTypeForIndexer(indexerConfig, configProvider.getBaseConfig().getDownloading().getNzbAccessType()); if (addingType == NzbAddingType.UPLOAD && accessTypeForIndexer == FileDownloadAccessType.PROXY) { DownloadResult result = nzbHandler.getFileByResult(FileDownloadAccessType.PROXY, SearchSource.INTERNAL, optionalResult.get()); //Uploading NZBs can only be done via proxying - searchResult = result.getDownloadEntity().getSearchResult(); if (result.isSuccessful()) { String externalId = addNzb(result.getContent(), result.getTitle(), categoryToSend); result.getDownloadEntity().setExternalId(externalId); @@ -150,8 +152,7 @@ public abstract class Downloader { missedNzbs.add(searchResult); } } else { - searchResult = searchResultRepository.getById(guid); - String externalId = addLink(nzbHandler.getDownloadLinkForSendingToDownloader(guid, false, DownloadType.NZB), searchResult.getTitle(), categoryToSend); + String externalId = addLink(nzbHandler.getDownloadLinkForSendingToDownloader(guid, false, DownloadType.NZB), searchResultTitle, categoryToSend); guidExternalIds.put(guid, externalId); addedNzbs.add(guid); } diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderEntry.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderEntry.java index a0611db59..af726959e 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderEntry.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderEntry.java @@ -20,10 +20,12 @@ import com.google.common.base.MoreObjects; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; import java.time.Instant; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class DownloaderEntry { diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderInstatiator.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderInstatiator.java new file mode 100644 index 000000000..3a36cc6a4 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderInstatiator.java @@ -0,0 +1,64 @@ +/* + * (C) Copyright 2022 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.downloading.downloaders; + +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.downloading.DownloaderType; +import org.nzbhydra.downloading.FileHandler; +import org.nzbhydra.downloading.IndexerSpecificDownloadExceptions; +import org.nzbhydra.downloading.downloaders.nzbget.NzbGet; +import org.nzbhydra.downloading.downloaders.sabnzbd.Sabnzbd; +import org.nzbhydra.searching.db.SearchResultRepository; +import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; +import org.nzbhydra.webaccess.Ssl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class DownloaderInstatiator { + + @Autowired + protected FileHandler nzbHandler; + @Autowired + protected SearchResultRepository searchResultRepository; + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + @Autowired + private IndexerSpecificDownloadExceptions indexerSpecificDownloadExceptions; + @Autowired + private ConfigProvider configProvider; + @Autowired + private HydraOkHttp3ClientHttpRequestFactory requestFactory; + @Autowired + private RestTemplate restTemplate; + @Autowired + private Ssl ssl; + + public Downloader instantiate(DownloaderType downloaderType) { + switch (downloaderType) { + case NZBGET -> { + return new NzbGet(nzbHandler, searchResultRepository, applicationEventPublisher, indexerSpecificDownloadExceptions, configProvider, ssl); + } + case SABNZBD -> { + return new Sabnzbd(nzbHandler, searchResultRepository, applicationEventPublisher, indexerSpecificDownloadExceptions, configProvider, restTemplate, requestFactory); + } + } + throw new RuntimeException("Unable to instantiate " + downloaderType); + } +} diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderProvider.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderProvider.java index 408f76edc..b4866b4c5 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderProvider.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderProvider.java @@ -19,15 +19,15 @@ package org.nzbhydra.downloading.downloaders; import org.nzbhydra.GenericResponse; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigChangedEvent; +import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.downloading.DownloaderConfig; -import org.nzbhydra.config.downloading.DownloaderType; +import org.nzbhydra.downloading.DownloaderType; import org.nzbhydra.downloading.downloaders.nzbget.NzbGet; import org.nzbhydra.downloading.downloaders.sabnzbd.Sabnzbd; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -49,21 +49,21 @@ public class DownloaderProvider implements InitializingBean { } @Autowired - private AutowireCapableBeanFactory beanFactory; + private ConfigProvider configProvider; @Autowired - private BaseConfig baseConfig; + private DownloaderInstatiator downloaderInstatiator; - private HashMap downloadersMap = new HashMap<>(); + private final HashMap downloadersMap = new HashMap<>(); @EventListener public void handleNewConfig(ConfigChangedEvent configChangedEvent) throws Exception { - baseConfig = configChangedEvent.getNewConfig(); afterPropertiesSet(); } @Override public void afterPropertiesSet() throws Exception { + final BaseConfig baseConfig = configProvider.getBaseConfig(); if (baseConfig.getDownloading().getDownloaders() != null) { List downloaderConfigs = baseConfig.getDownloading().getDownloaders(); downloadersMap.clear(); @@ -71,7 +71,7 @@ public class DownloaderProvider implements InitializingBean { for (DownloaderConfig downloaderConfig : downloaderConfigs) { logger.info("Initializing downloader {}", downloaderConfig.getName()); try { - Downloader downloader = beanFactory.createBean(downloaderClasses.get(downloaderConfig.getDownloaderType())); + Downloader downloader = downloaderInstatiator.instantiate(downloaderConfig.getDownloaderType()); downloader.initialize(downloaderConfig); downloadersMap.put(downloaderConfig.getName().toLowerCase(), downloader); } catch (Exception e) { @@ -88,7 +88,7 @@ public class DownloaderProvider implements InitializingBean { } public GenericResponse checkConnection(DownloaderConfig downloaderConfig) { - Downloader downloader = beanFactory.createBean(downloaderClasses.get(downloaderConfig.getDownloaderType())); + Downloader downloader = downloaderInstatiator.instantiate(downloaderConfig.getDownloaderType()); downloader.initialize(downloaderConfig); return downloader.checkConnection(); } diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatusRetrieval.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatusRetrieval.java index 9bd4d1e7f..8198f614a 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatusRetrieval.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatusRetrieval.java @@ -25,7 +25,6 @@ import org.springframework.stereotype.Component; import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Component public class DownloaderStatusRetrieval { @@ -41,8 +40,8 @@ public class DownloaderStatusRetrieval { public DownloaderStatus getStatus() { Collection allDownloaders = downloaderProvider.getAllDownloaders(); List enabledDownloaders = allDownloaders.stream() - .filter(Downloader::isEnabled) - .collect(Collectors.toList()); + .filter(Downloader::isEnabled) + .toList(); if (enabledDownloaders.isEmpty()) { return new DownloaderStatus(); } @@ -50,7 +49,7 @@ public class DownloaderStatusRetrieval { .filter(x -> enabledDownloaders.size() == 1 || x.getName().equals(configProvider.getBaseConfig().getDownloading().getPrimaryDownloader())) .findFirst(); - if (!downloader.isPresent()) { + if (downloader.isEmpty()) { logger.error("Unable to determine to choose downloader for which to retrieve status."); return new DownloaderStatus(); } diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderWebSocket.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderWebSocket.java index e7d7135a6..e65b59144 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderWebSocket.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderWebSocket.java @@ -16,7 +16,7 @@ package org.nzbhydra.downloading.downloaders; -import org.nzbhydra.ShutdownEvent; +import jakarta.annotation.PreDestroy; import org.nzbhydra.config.ConfigChangedEvent; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.logging.LoggingMarkers; @@ -138,8 +138,8 @@ public class DownloaderWebSocket { } } - @EventListener - public void onShutdown(ShutdownEvent event) { + @PreDestroy + public void onShutdown() { if (scheduledFuture != null) { scheduledFuture.cancel(true); scheduledFuture = null; diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/nzbget/NzbGet.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/nzbget/NzbGet.java index 11bbeebc5..3ce62a4bc 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/nzbget/NzbGet.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/nzbget/NzbGet.java @@ -23,17 +23,19 @@ import org.nzbhydra.GenericResponse; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.downloading.DownloaderConfig; import org.nzbhydra.downloading.FileDownloadStatus; +import org.nzbhydra.downloading.FileHandler; +import org.nzbhydra.downloading.IndexerSpecificDownloadExceptions; import org.nzbhydra.downloading.downloaders.Converters; import org.nzbhydra.downloading.downloaders.Downloader; import org.nzbhydra.downloading.downloaders.DownloaderEntry; import org.nzbhydra.downloading.downloaders.DownloaderStatus; import org.nzbhydra.downloading.exceptions.DownloaderException; import org.nzbhydra.logging.LoggingMarkers; +import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.webaccess.Ssl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.web.util.UriComponentsBuilder; import java.net.MalformedURLException; @@ -48,7 +50,6 @@ import java.util.Optional; import java.util.stream.Collectors; @SuppressWarnings({"unchecked", "RedundantCast"}) -@Component public class NzbGet extends Downloader { private static final Map NZBGET_STATUS_TO_HYDRA_STATUS = new HashMap<>(); @@ -80,10 +81,12 @@ public class NzbGet extends Downloader { } - @Autowired - private ConfigProvider configProvider; - @Autowired - private Ssl ssl; + private final Ssl ssl; + + public NzbGet(FileHandler nzbHandler, SearchResultRepository searchResultRepository, ApplicationEventPublisher applicationEventPublisher, IndexerSpecificDownloadExceptions indexerSpecificDownloadExceptions, ConfigProvider configProvider, Ssl ssl) { + super(nzbHandler, searchResultRepository, applicationEventPublisher, indexerSpecificDownloadExceptions, configProvider); + this.ssl = ssl; + } private static final Logger logger = LoggerFactory.getLogger(NzbGet.class); private JsonRpcHttpClient client; @@ -103,7 +106,8 @@ public class NzbGet extends Downloader { headers.put("Authorization", "Basic " + BaseEncoding.base64().encode((downloaderConfig.getUsername().get() + ":" + downloaderConfig.getPassword().get()).getBytes())); } client = new JsonRpcHttpClient(builder.build().toUri().toURL(), headers); - final Ssl.SslVerificationState verificationState = ssl.getVerificationStateForHost(builder.build().getHost()); + final String host = builder.build().getHost(); + final Ssl.SslVerificationState verificationState = ssl.getVerificationStateForHost(host); if (verificationState == Ssl.SslVerificationState.ENABLED) { client.setSslContext(ssl.getCaCertsContext()); } else { @@ -199,7 +203,7 @@ public class NzbGet extends Downloader { protected void fillStatusFromQueue(DownloaderStatus status) throws DownloaderException { ArrayList> queue = callNzbget("listgroups", new Object[]{0}); - List> nzbs = queue.stream().filter(x -> "NZB".equals(x.get("Kind"))).collect(Collectors.toList()); + List> nzbs = queue.stream().filter(x -> "NZB".equals(x.get("Kind"))).toList(); if (!nzbs.isEmpty()) { status.setElementsInQueue(queue.size()); diff --git a/core/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/Sabnzbd.java b/core/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/Sabnzbd.java index 793bb7cbc..b02ad170c 100644 --- a/core/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/Sabnzbd.java +++ b/core/src/main/java/org/nzbhydra/downloading/downloaders/sabnzbd/Sabnzbd.java @@ -13,8 +13,11 @@ import org.joda.time.format.PeriodFormatter; import org.joda.time.format.PeriodFormatterBuilder; import org.nzbhydra.GenericResponse; import org.nzbhydra.Jackson; -import org.nzbhydra.config.downloading.DownloaderType; +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.downloading.DownloaderType; import org.nzbhydra.downloading.FileDownloadStatus; +import org.nzbhydra.downloading.FileHandler; +import org.nzbhydra.downloading.IndexerSpecificDownloadExceptions; import org.nzbhydra.downloading.downloaders.Converters; import org.nzbhydra.downloading.downloaders.Downloader; import org.nzbhydra.downloading.downloaders.DownloaderEntry; @@ -30,14 +33,14 @@ import org.nzbhydra.downloading.exceptions.DownloaderException; import org.nzbhydra.downloading.exceptions.DownloaderUnreachableException; import org.nzbhydra.downloading.exceptions.DuplicateNzbException; import org.nzbhydra.logging.LoggingMarkers; +import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.client.UnknownContentTypeException; @@ -53,7 +56,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -@Component public class Sabnzbd extends Downloader { private static final Logger logger = LoggerFactory.getLogger(Sabnzbd.class); @@ -82,10 +84,14 @@ public class Sabnzbd extends Downloader { private Instant lastErrorLogged; - @Autowired - private RestTemplate restTemplate; - @Autowired - private HydraOkHttp3ClientHttpRequestFactory requestFactory; + private final RestTemplate restTemplate; + private final HydraOkHttp3ClientHttpRequestFactory requestFactory; + + public Sabnzbd(FileHandler nzbHandler, SearchResultRepository searchResultRepository, ApplicationEventPublisher applicationEventPublisher, IndexerSpecificDownloadExceptions indexerSpecificDownloadExceptions, ConfigProvider configProvider, RestTemplate restTemplate, HydraOkHttp3ClientHttpRequestFactory requestFactory) { + super(nzbHandler, searchResultRepository, applicationEventPublisher, indexerSpecificDownloadExceptions, configProvider); + this.restTemplate = restTemplate; + this.requestFactory = requestFactory; + } private UriComponentsBuilder getBaseUrl() { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(downloaderConfig.getUrl()).pathSegment("api"); @@ -151,7 +157,7 @@ public class Sabnzbd extends Downloader { .url(urlBuilder.toUriString()) .post(formBody) .build(); - OkHttpClient client = requestFactory.getOkHttpClientBuilder(urlBuilder.build().encode().toUri()).build(); + OkHttpClient client = requestFactory.getOkHttpClient(urlBuilder.build().encode().toUri().getHost()); try (Response response = client.newCall(request).execute(); ResponseBody body = response.body()) { if (!response.isSuccessful()) { throw new DownloaderException("Downloader returned status code " + response.code() + " and message " + response.message()); @@ -307,7 +313,7 @@ public class Sabnzbd extends Downloader { final ResponseEntity exchange = restTemplate.exchange(baseUrl.queryParam("mode", "queue").build().toUri().toString(), HttpMethod.GET, null, QueueResponse.class); if (!exchange.getStatusCode().is2xxSuccessful()) { logger.info("Connection check with sabNZBd using URL {}\n failed: Response body: {}", baseUrl.toUriString(), exchange.getBody().toString()); - return new GenericResponse(false, "Connection check with sabnzbd failed. Response message: " + exchange.getStatusCode().getReasonPhrase() + ".The log may contain more infos."); + return new GenericResponse(false, "Connection check with sabnzbd failed. Status code: " + exchange.getStatusCode() + ".The log may contain more infos."); } if (exchange.getBody() == null || exchange.getBody().getQueue() == null) { logger.info("Connection check with sabNZBd using URL {} failed. Unable to parse response.", baseUrl.toUriString()); diff --git a/core/src/main/java/org/nzbhydra/downloading/nzbs/NzbHandlingWeb.java b/core/src/main/java/org/nzbhydra/downloading/nzbs/NzbHandlingWeb.java index 80ade75bf..a140eb1a4 100644 --- a/core/src/main/java/org/nzbhydra/downloading/nzbs/NzbHandlingWeb.java +++ b/core/src/main/java/org/nzbhydra/downloading/nzbs/NzbHandlingWeb.java @@ -20,12 +20,12 @@ import org.nzbhydra.GenericResponse; import org.nzbhydra.api.WrongApiKeyException; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.downloading.FileHandler; import org.nzbhydra.downloading.FileZipResponse; import org.nzbhydra.downloading.InvalidSearchResultIdException; import org.nzbhydra.indexers.NfoResult; import org.nzbhydra.indexers.exceptions.IndexerAccessException; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; diff --git a/core/src/main/java/org/nzbhydra/downloading/torrents/SaveOrSendTorrentsResponse.java b/core/src/main/java/org/nzbhydra/downloading/torrents/SaveOrSendTorrentsResponse.java index 2b978816b..907600356 100644 --- a/core/src/main/java/org/nzbhydra/downloading/torrents/SaveOrSendTorrentsResponse.java +++ b/core/src/main/java/org/nzbhydra/downloading/torrents/SaveOrSendTorrentsResponse.java @@ -19,10 +19,12 @@ package org.nzbhydra.downloading.torrents; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.Collection; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class SaveOrSendTorrentsResponse { diff --git a/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentFileHandler.java b/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentFileHandler.java index ebe096b6e..6d9e79e2c 100644 --- a/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentFileHandler.java +++ b/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentFileHandler.java @@ -20,6 +20,7 @@ import com.google.common.io.Files; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.downloading.FileDownloadAccessType; import org.nzbhydra.downloading.DownloadResult; import org.nzbhydra.downloading.FileHandler; @@ -27,7 +28,6 @@ import org.nzbhydra.downloading.InvalidSearchResultIdException; import org.nzbhydra.downloading.MagnetLinkRedirectException; import org.nzbhydra.searching.db.SearchResultEntity; import org.nzbhydra.searching.db.SearchResultRepository; -import org.nzbhydra.searching.searchrequests.SearchRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -58,12 +58,12 @@ public class TorrentFileHandler { private SearchResultRepository searchResultRepository; @Transactional - public DownloadResult getTorrentByGuid(long guid, FileDownloadAccessType accessType, SearchRequest.SearchSource accessSource) throws InvalidSearchResultIdException { + public DownloadResult getTorrentByGuid(long guid, FileDownloadAccessType accessType, SearchSource accessSource) throws InvalidSearchResultIdException { //Get result. if link contains magnet: return redirect to magnet URI. otherwise return file Optional optionalResult = searchResultRepository.findById(guid); - if (!optionalResult.isPresent()) { + if (optionalResult.isEmpty()) { logger.error("Download request with invalid/outdated GUID {}", guid); - throw new InvalidSearchResultIdException(guid, accessSource == SearchRequest.SearchSource.INTERNAL); + throw new InvalidSearchResultIdException(guid, accessSource == SearchSource.INTERNAL); } SearchResultEntity result = optionalResult.get(); logger.info("Download request for \"{}\" from indexer {}", result.getTitle(), result.getIndexer().getName()); @@ -85,7 +85,7 @@ public class TorrentFileHandler { DownloadResult result; boolean successful = false; try { - result = getTorrentByGuid(guid, FileDownloadAccessType.PROXY, SearchRequest.SearchSource.INTERNAL); + result = getTorrentByGuid(guid, FileDownloadAccessType.PROXY, SearchSource.INTERNAL); } catch (InvalidSearchResultIdException e) { logger.error("Unable to find result with ID {}", guid); failedIds.add(guid); @@ -140,7 +140,7 @@ public class TorrentFileHandler { } private boolean saveToBlackHole(DownloadResult result, URI magnetLinkUri) { - if (!configProvider.getBaseConfig().getDownloading().getSaveTorrentsTo().isPresent()) { + if (configProvider.getBaseConfig().getDownloading().getSaveTorrentsTo().isEmpty()) { logger.error("Torrent black hole folder not set"); return false; } diff --git a/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentHandlingWeb.java b/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentHandlingWeb.java index 7f3aef319..33fefacf4 100644 --- a/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentHandlingWeb.java +++ b/core/src/main/java/org/nzbhydra/downloading/torrents/TorrentHandlingWeb.java @@ -20,9 +20,9 @@ import com.google.common.collect.Sets; import org.nzbhydra.api.WrongApiKeyException; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.downloading.DownloadResult; import org.nzbhydra.downloading.InvalidSearchResultIdException; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; diff --git a/core/src/main/java/org/nzbhydra/externaltools/AddDialogInfo.java b/core/src/main/java/org/nzbhydra/externaltools/AddDialogInfo.java index 0e29e8ab7..ef331ff39 100644 --- a/core/src/main/java/org/nzbhydra/externaltools/AddDialogInfo.java +++ b/core/src/main/java/org/nzbhydra/externaltools/AddDialogInfo.java @@ -18,8 +18,10 @@ package org.nzbhydra.externaltools; import lombok.AllArgsConstructor; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor public class AddDialogInfo { diff --git a/core/src/main/java/org/nzbhydra/externaltools/ExternalTools.java b/core/src/main/java/org/nzbhydra/externaltools/ExternalTools.java index f01624b48..37db7b7fb 100644 --- a/core/src/main/java/org/nzbhydra/externaltools/ExternalTools.java +++ b/core/src/main/java/org/nzbhydra/externaltools/ExternalTools.java @@ -29,10 +29,11 @@ import org.apache.commons.lang3.StringUtils; import org.nzbhydra.Jackson; import org.nzbhydra.api.ExternalApi; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; import org.nzbhydra.logging.LoggingMarkers; -import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.web.UrlCalculator; import org.nzbhydra.webaccess.WebAccess; import org.nzbhydra.webaccess.WebAccessException; @@ -58,7 +59,7 @@ import java.util.stream.Stream; @Component public class ExternalTools { - private static final TypeReference> LIST_TYPE_REFERENCE = new TypeReference>() { + private static final TypeReference> LIST_TYPE_REFERENCE = new TypeReference<>() { }; private enum BackendType { @@ -91,7 +92,7 @@ public class ExternalTools { if (addRequest.isConfigureForUsenet()) { final boolean anyUsenetIndexerEnabled = configProvider.getBaseConfig().getIndexers().stream() - .filter(x -> x.getEnabledForSearchSource().meets(SearchRequest.SearchSource.API)) + .filter(x -> SearchSource.API.meets(x.getEnabledForSearchSource())) .filter(x -> x.getState() == IndexerConfig.State.ENABLED) .anyMatch(x -> x.getSearchModuleType() != SearchModuleType.TORZNAB); if (!anyUsenetIndexerEnabled) { @@ -109,9 +110,9 @@ public class ExternalTools { logger.info("Received request to configure {} at URL {} with add type {} for usenet: {} and torrents: {}", addRequest.getExternalTool(), addRequest.getXdarrHost(), addRequest.getAddType(), addRequest.isConfigureForUsenet(), addRequest.isConfigureForTorrents()); final List availableIndexers = configProvider.getBaseConfig().getIndexers().stream() - .filter(x -> (x.getState() == IndexerConfig.State.ENABLED || addRequest.isAddDisabledIndexers()) && x.isConfigComplete() && x.isAllCapsChecked()) - .filter(x -> x.getEnabledForSearchSource().meets(SearchRequest.SearchSource.API)) - .collect(Collectors.toList()); + .filter(x -> (x.getState() == IndexerConfig.State.ENABLED || addRequest.isAddDisabledIndexers()) && x.isConfigComplete() && x.isAllCapsChecked()) + .filter(x -> SearchSource.API.meets(x.getEnabledForSearchSource())) + .toList(); final Optional maxPriority = availableIndexers.stream().map(IndexerConfig::getScore).max(Comparator.naturalOrder()); if (addRequest.getAddType() == AddRequest.AddType.PER_INDEXER && addRequest.isUseHydraPriorities() && maxPriority.isPresent() && maxPriority.get() > 51) { @@ -130,7 +131,7 @@ public class ExternalTools { if (addRequest.getAddType() == AddRequest.AddType.SINGLE) { executeConfigurationRequest(addRequest, BackendType.Newznab, null); } else { - final List availableTorznabIndexers = availableIndexers.stream().filter(x -> x.getSearchModuleType() != SearchModuleType.TORZNAB).collect(Collectors.toList()); + final List availableTorznabIndexers = availableIndexers.stream().filter(x -> x.getSearchModuleType() != SearchModuleType.TORZNAB).toList(); for (IndexerConfig indexer : availableTorznabIndexers) { executeConfigurationRequest(addRequest, BackendType.Newznab, indexer); } @@ -140,7 +141,7 @@ public class ExternalTools { if (addRequest.getAddType() == AddRequest.AddType.SINGLE) { executeConfigurationRequest(addRequest, BackendType.Torznab, null); } else { - final List availableUsenetIndexers = availableIndexers.stream().filter(x -> x.getSearchModuleType() == SearchModuleType.TORZNAB).collect(Collectors.toList()); + final List availableUsenetIndexers = availableIndexers.stream().filter(x -> x.getSearchModuleType() == SearchModuleType.TORZNAB).toList(); for (IndexerConfig indexer : availableUsenetIndexers) { executeConfigurationRequest(addRequest, BackendType.Torznab, indexer); } @@ -401,7 +402,7 @@ public class ExternalTools { response = webAccess.callUrl(url, getAuthHeaders(addRequest)); logger.debug(LoggingMarkers.EXTERNAL_TOOLS, "Received response body: {}", response); - final List requestResponse = Jackson.JSON_MAPPER.readValue(response, new TypeReference>() { + final List requestResponse = Jackson.JSON_MAPPER.readValue(response, new TypeReference<>() { }); final List nzbhydraIndexers = requestResponse.stream().filter(x -> x.getName().contains(addRequest.getNzbhydraName())).collect(Collectors.toList()); @@ -477,6 +478,7 @@ public class ExternalTools { } @Data +@ReflectionMarker @JsonIgnoreProperties(ignoreUnknown = true) public static class XdarrIndexer { @@ -501,6 +503,7 @@ public class ExternalTools { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @@ -511,6 +514,7 @@ public class ExternalTools { } @Data +@ReflectionMarker public static class XdarrAddRequestResponse { private boolean isWarning; private String propertyName; diff --git a/core/src/main/java/org/nzbhydra/fortests/DebugWeb.java b/core/src/main/java/org/nzbhydra/fortests/DebugWeb.java index bee670423..952f06edd 100644 --- a/core/src/main/java/org/nzbhydra/fortests/DebugWeb.java +++ b/core/src/main/java/org/nzbhydra/fortests/DebugWeb.java @@ -16,6 +16,7 @@ package org.nzbhydra.fortests; +import jakarta.servlet.http.HttpServletRequest; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.web.UrlCalculator; import org.springframework.beans.factory.annotation.Autowired; @@ -23,7 +24,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; import java.net.URL; import java.util.Enumeration; diff --git a/core/src/main/java/org/nzbhydra/fortests/NewznabItemData.java b/core/src/main/java/org/nzbhydra/fortests/NewznabItemData.java index 299f77b95..8d37c6535 100644 --- a/core/src/main/java/org/nzbhydra/fortests/NewznabItemData.java +++ b/core/src/main/java/org/nzbhydra/fortests/NewznabItemData.java @@ -2,11 +2,13 @@ package org.nzbhydra.fortests; import lombok.Builder; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.time.Instant; import java.util.List; @Data +@ReflectionMarker @Builder public class NewznabItemData { diff --git a/core/src/main/java/org/nzbhydra/fortests/NewznabResponseData.java b/core/src/main/java/org/nzbhydra/fortests/NewznabResponseData.java index 5d217429f..ac4eb25f1 100644 --- a/core/src/main/java/org/nzbhydra/fortests/NewznabResponseData.java +++ b/core/src/main/java/org/nzbhydra/fortests/NewznabResponseData.java @@ -3,10 +3,12 @@ package org.nzbhydra.fortests; import lombok.Builder; import lombok.Data; import lombok.Singular; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.List; @Data +@ReflectionMarker @Builder public class NewznabResponseData { diff --git a/core/src/main/java/org/nzbhydra/genericstorage/GenericStorage.java b/core/src/main/java/org/nzbhydra/genericstorage/GenericStorage.java index a6e6f2310..43e6d357d 100644 --- a/core/src/main/java/org/nzbhydra/genericstorage/GenericStorage.java +++ b/core/src/main/java/org/nzbhydra/genericstorage/GenericStorage.java @@ -2,6 +2,7 @@ package org.nzbhydra.genericstorage; import com.fasterxml.jackson.core.JsonProcessingException; import org.nzbhydra.Jackson; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -15,12 +16,14 @@ public class GenericStorage { @Autowired private ConfigProvider configProvider; + @Autowired + private BaseConfigHandler baseConfigHandler; public void save(String key, T value) { Map genericStorage = configProvider.getBaseConfig().getGenericStorage(); try { genericStorage.put(key, Jackson.JSON_MAPPER.writeValueAsString(value)); - configProvider.getBaseConfig().save(true); + baseConfigHandler.save(true); } catch (JsonProcessingException e) { throw new RuntimeException("Error writing data as JSON", e); } @@ -36,7 +39,7 @@ public class GenericStorage { public void remove(String key) { configProvider.getBaseConfig().getGenericStorage().remove(key); - configProvider.getBaseConfig().save(true); + baseConfigHandler.save(true); } diff --git a/core/src/main/java/org/nzbhydra/genericstorage/GenericStorageWeb.java b/core/src/main/java/org/nzbhydra/genericstorage/GenericStorageWeb.java index 8c94c0ec0..d2b0f6f63 100644 --- a/core/src/main/java/org/nzbhydra/genericstorage/GenericStorageWeb.java +++ b/core/src/main/java/org/nzbhydra/genericstorage/GenericStorageWeb.java @@ -16,6 +16,7 @@ package org.nzbhydra.genericstorage; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -24,8 +25,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; - @RestController public class GenericStorageWeb { @@ -33,7 +32,7 @@ public class GenericStorageWeb { private GenericStorage genericStorage; @RequestMapping(value = "/internalapi/genericstorage/{key}", method = RequestMethod.GET) - public Object get(@PathVariable String key, @RequestParam boolean forUser, HttpServletRequest request) { + public Object get(@PathVariable String key, @RequestParam(required = false) boolean forUser, HttpServletRequest request) { String keyToUse = key; if (forUser && request.getRemoteUser() != null) { keyToUse = key + "-" + request.getRemoteUser(); @@ -42,7 +41,7 @@ public class GenericStorageWeb { } @RequestMapping(value = "/internalapi/genericstorage/{key}", method = RequestMethod.PUT) - public void put(@PathVariable String key, @RequestParam boolean forUser, @RequestBody String data, HttpServletRequest request) { + public void put(@PathVariable String key, @RequestParam(required = false) boolean forUser, @RequestBody String data, HttpServletRequest request) { String keyToUse = key; if (forUser && request.getRemoteUser() != null) { keyToUse = key + "-" + request.getRemoteUser(); diff --git a/core/src/main/java/org/nzbhydra/historystats/History.java b/core/src/main/java/org/nzbhydra/historystats/History.java index 49426dc80..37f7d016f 100644 --- a/core/src/main/java/org/nzbhydra/historystats/History.java +++ b/core/src/main/java/org/nzbhydra/historystats/History.java @@ -1,13 +1,19 @@ package org.nzbhydra.historystats; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.hibernate.Hibernate; +import org.nzbhydra.downloading.FileDownloadEntity; import org.nzbhydra.historystats.stats.HistoryRequest; import org.nzbhydra.indexers.IndexerSearchEntity; import org.nzbhydra.indexers.IndexerSearchRepository; import org.nzbhydra.searching.db.SearchEntity; import org.nzbhydra.searching.db.SearchRepository; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.web.SessionStorage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -16,11 +22,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.Query; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -35,9 +38,9 @@ import java.util.Set; @Component public class History { - public static final String DOWNLOAD_TABLE = "INDEXERNZBDOWNLOAD left join SEARCHRESULT on INDEXERNZBDOWNLOAD.SEARCH_RESULT_ID = SEARCHRESULT.ID LEFT JOIN INDEXER ON SEARCHRESULT.INDEXER_ID = INDEXER.ID"; - public static final String SEARCH_TABLE = "SEARCH"; - public static final String NOTIFICATION_TABLE = "NOTIFICATION"; + public static final String DOWNLOAD_TABLE = "INDEXERNZBDOWNLOAD x left join SEARCHRESULT s on x.SEARCH_RESULT_ID = s.ID LEFT JOIN INDEXER i ON s.INDEXER_ID = i.ID"; + public static final String SEARCH_TABLE = "SEARCH x"; + public static final String NOTIFICATION_TABLE = "NOTIFICATION x"; @PersistenceContext private EntityManager entityManager; @@ -46,6 +49,7 @@ public class History { @Autowired private IndexerSearchRepository indexerSearchRepository; + @Transactional public Page getHistory(HistoryRequest requestData, String tableName, Class resultClass) { Map parameters = new HashMap<>(); @@ -108,11 +112,12 @@ public class History { String paging = String.format(" LIMIT %d OFFSET %d", requestData.getLimit(), (requestData.getPage() - 1) * requestData.getLimit()); - String selectQuerySql = "SELECT * FROM " + tableName + whereConditions + sort + paging; - String countQuerySql = "SELECT COUNT(*) FROM " + tableName + whereConditions; + String selectQuerySql = "SELECT x.* FROM " + tableName + whereConditions + sort + paging; + String countQuerySql = "SELECT COUNT(x.*) FROM " + tableName + whereConditions; Query selectQuery = entityManager.createNativeQuery(selectQuerySql, resultClass); Query countQuery = entityManager.createNativeQuery(countQuerySql); + for (Entry entry : parameters.entrySet()) { selectQuery.setParameter(entry.getKey(), entry.getValue()); countQuery.setParameter(entry.getKey(), entry.getValue()); @@ -126,8 +131,15 @@ public class History { pageable = PageRequest.of(requestData.getPage() - 1, requestData.getLimit(), sortModel.getSortMode() == 1 ? Sort.Direction.ASC : Sort.Direction.DESC, sortModel.getColumn()); } - BigInteger count = (BigInteger) countQuery.getSingleResult(); - return new PageImpl<>(resultList, pageable, count.longValue()); + Long count = (Long) countQuery.getSingleResult(); + if (resultClass == SearchEntity.class) { + resultList.forEach(x -> Hibernate.initialize(((SearchEntity) x).getIdentifiers())); + } else if (resultClass == FileDownloadEntity.class) { + Hibernate.initialize(resultList); + resultList.forEach(x -> Hibernate.initialize(((FileDownloadEntity) x).getSearchResult())); + + } + return new PageImpl<>(resultList, pageable, count); } public List getHistoryForSearching() { @@ -162,6 +174,7 @@ public class History { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class SearchDetails { @@ -173,6 +186,7 @@ public class History { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class IndexerSearchTO { diff --git a/core/src/main/java/org/nzbhydra/historystats/HistoryWeb.java b/core/src/main/java/org/nzbhydra/historystats/HistoryWeb.java index 0c2342f36..482e29c2d 100644 --- a/core/src/main/java/org/nzbhydra/historystats/HistoryWeb.java +++ b/core/src/main/java/org/nzbhydra/historystats/HistoryWeb.java @@ -1,21 +1,27 @@ package org.nzbhydra.historystats; +import jakarta.servlet.http.HttpServletRequest; +import org.nzbhydra.Jackson; import org.nzbhydra.downloading.FileDownloadEntity; +import org.nzbhydra.downloading.FileDownloadEntityTO; import org.nzbhydra.historystats.History.SearchDetails; import org.nzbhydra.historystats.stats.HistoryRequest; import org.nzbhydra.notifications.NotificationEntity; +import org.nzbhydra.notifications.NotificationEntityTO; import org.nzbhydra.searching.db.SearchEntity; +import org.nzbhydra.searching.db.SearchEntityTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.http.MediaType; import org.springframework.security.access.annotation.Secured; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @@ -25,12 +31,18 @@ public class HistoryWeb { private History history; @Secured({"ROLE_STATS"}) + @Transactional @RequestMapping(value = "/internalapi/history/searches", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public Page searchHistory(@RequestBody HistoryRequest requestData) { - return history.getHistory(requestData, History.SEARCH_TABLE, SearchEntity.class); + public Page searchHistory(@RequestBody HistoryRequest requestData) { + final Page page = history.getHistory(requestData, History.SEARCH_TABLE, SearchEntity.class); + final List searchEntityTOS = page.getContent().stream() + .map(x -> Jackson.JSON_MAPPER.convertValue(x, SearchEntityTO.class)) + .toList(); + return new PageImpl<>(searchEntityTOS); } @Secured({"ROLE_STATS"}) + @Transactional @RequestMapping(value = "/internalapi/history/searches/details/{searchId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public SearchDetails searchHistoryDetails(@PathVariable int searchId) { return history.getSearchDetails(searchId); @@ -38,21 +50,34 @@ public class HistoryWeb { @Secured({"ROLE_USER"}) @RequestMapping(value = "/internalapi/history/searches/forsearching", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) - public List searchHistoryForSearchPage(HttpServletRequest request) { - return history.getHistoryForSearching(); + public List searchHistoryForSearchPage(HttpServletRequest request) { + return history.getHistoryForSearching().stream() + .map(x -> Jackson.JSON_MAPPER.convertValue(x, SearchEntityTO.class)) + .toList(); } @Secured({"ROLE_STATS"}) @RequestMapping(value = "/internalapi/history/downloads", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public Page downloadHistory(@RequestBody HistoryRequest requestData) { - return history.getHistory(requestData, History.DOWNLOAD_TABLE, FileDownloadEntity.class); + public Page downloadHistory(@RequestBody HistoryRequest requestData) { + final Page page = history.getHistory(requestData, History.DOWNLOAD_TABLE, FileDownloadEntity.class); + final List downloadEntityTOS = page + .stream() + .map(x -> Jackson.JSON_MAPPER.convertValue(x, FileDownloadEntityTO.class)) + .toList(); + return new PageImpl<>(downloadEntityTOS); } @Secured({"ROLE_STATS"}) @RequestMapping(value = "/internalapi/history/notifications", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public Page notificationHistory(@RequestBody HistoryRequest requestData) { - return history.getHistory(requestData, History.NOTIFICATION_TABLE, NotificationEntity.class); + public Page notificationHistory(@RequestBody HistoryRequest requestData) { + final Page page = history.getHistory(requestData, History.NOTIFICATION_TABLE, NotificationEntity.class); + + final List tos = page + .stream() + .map(x -> Jackson.JSON_MAPPER.convertValue(x, NotificationEntityTO.class)) + .toList(); + return new PageImpl<>(tos); } } diff --git a/core/src/main/java/org/nzbhydra/historystats/Stats.java b/core/src/main/java/org/nzbhydra/historystats/Stats.java index df286d2d4..38ad265c7 100644 --- a/core/src/main/java/org/nzbhydra/historystats/Stats.java +++ b/core/src/main/java/org/nzbhydra/historystats/Stats.java @@ -1,10 +1,28 @@ package org.nzbhydra.historystats; import com.google.common.base.Stopwatch; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; -import org.nzbhydra.historystats.stats.*; -import org.nzbhydra.indexers.*; +import org.nzbhydra.historystats.stats.AverageResponseTime; +import org.nzbhydra.historystats.stats.CountPerDayOfWeek; +import org.nzbhydra.historystats.stats.CountPerHourOfDay; +import org.nzbhydra.historystats.stats.DownloadOrSearchSharePerUserOrIp; +import org.nzbhydra.historystats.stats.DownloadPerAge; +import org.nzbhydra.historystats.stats.DownloadPerAgeStats; +import org.nzbhydra.historystats.stats.IndexerApiAccessStatsEntry; +import org.nzbhydra.historystats.stats.IndexerDownloadShare; +import org.nzbhydra.historystats.stats.IndexerScore; +import org.nzbhydra.historystats.stats.StatsRequest; +import org.nzbhydra.historystats.stats.SuccessfulDownloadsPerIndexer; +import org.nzbhydra.historystats.stats.UserAgentShare; +import org.nzbhydra.indexers.Indexer; +import org.nzbhydra.indexers.IndexerAccessResult; +import org.nzbhydra.indexers.IndexerApiAccessEntityShortRepository; +import org.nzbhydra.indexers.IndexerEntity; +import org.nzbhydra.indexers.IndexerRepository; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.searching.SearchModuleProvider; import org.nzbhydra.searching.db.SearchResultRepository; @@ -16,13 +34,22 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.Query; -import java.math.BigInteger; -import java.util.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.*; +import java.util.OptionalDouble; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @SuppressWarnings("OptionalGetWithoutIsPresent") @@ -106,26 +133,26 @@ public class Stats { if (statsRequest.isSearchSharesPerUser()) { - BigInteger countSearchesWithData = (BigInteger) entityManager.createNativeQuery("SELECT count(*) FROM SEARCH t WHERE t.USERNAME IS NOT NULL").getSingleResult(); + Long countSearchesWithData = (Long) entityManager.createNativeQuery("SELECT count(*) FROM SEARCH t WHERE t.USERNAME IS NOT NULL").getSingleResult(); if (countSearchesWithData.intValue() > 0) { futures.add(executor.submit(() -> statsResponse.setSearchSharesPerUser(downloadsOrSearchesPerUserOrIp(statsRequest, "SEARCH", "USERNAME")))); } } if (statsRequest.isDownloadSharesPerUser()) { - BigInteger countDownloadsWithData = (BigInteger) entityManager.createNativeQuery("SELECT count(*) FROM INDEXERNZBDOWNLOAD t WHERE t.USERNAME IS NOT NULL").getSingleResult(); - if (countDownloadsWithData.intValue() > 0) { + Long countDownloadsWithData = (Long) entityManager.createNativeQuery("SELECT count(*) FROM INDEXERNZBDOWNLOAD t WHERE t.USERNAME IS NOT NULL").getSingleResult(); + if (countDownloadsWithData > 0) { futures.add(executor.submit(() -> statsResponse.setDownloadSharesPerUser(downloadsOrSearchesPerUserOrIp(statsRequest, "INDEXERNZBDOWNLOAD", "USERNAME")))); } } if (statsRequest.isSearchSharesPerIp()) { - BigInteger countSearchesWithData = (BigInteger) entityManager.createNativeQuery("SELECT count(*) FROM SEARCH t WHERE t.IP IS NOT NULL").getSingleResult(); - if (countSearchesWithData.intValue() > 0) { + Long countSearchesWithData = (Long) entityManager.createNativeQuery("SELECT count(*) FROM SEARCH t WHERE t.IP IS NOT NULL").getSingleResult(); + if (countSearchesWithData > 0) { futures.add(executor.submit(() -> statsResponse.setSearchSharesPerIp(downloadsOrSearchesPerUserOrIp(statsRequest, "SEARCH", "IP")))); } } if (statsRequest.isDownloadSharesPerIp()) { - BigInteger countDownloadsWithData = (BigInteger) entityManager.createNativeQuery("SELECT count(*) FROM INDEXERNZBDOWNLOAD t WHERE t.IP IS NOT NULL").getSingleResult(); - if (countDownloadsWithData.intValue() > 0) { + Long countDownloadsWithData = (Long) entityManager.createNativeQuery("SELECT count(*) FROM INDEXERNZBDOWNLOAD t WHERE t.IP IS NOT NULL").getSingleResult(); + if (countDownloadsWithData > 0) { futures.add(executor.submit(() -> statsResponse.setDownloadSharesPerIp(downloadsOrSearchesPerUserOrIp(statsRequest, "INDEXERNZBDOWNLOAD", "IP")))); } } @@ -193,8 +220,8 @@ public class Stats { if (!indexerNamesToInclude.contains(indexerName)) { continue; } - long total = ((BigInteger) resultSet[1]).longValue(); - long countAll = ((BigInteger) resultSet[2]).longValue(); + long total = ((Long) resultSet[1]).longValue(); + long countAll = ((Long) resultSet[2]).longValue(); float share = total > 0 ? (100F / ((float) countAll / total)) : 0F; indexerDownloadShares.add(new IndexerDownloadShare(indexerName, total, share)); } @@ -219,7 +246,7 @@ public class Stats { Query query = entityManager.createNativeQuery(sql); List resultList = query.getResultList(); Set indexerNamesToInclude = searchModuleProvider.getIndexers().stream().filter(x -> x.getConfig().getState() == IndexerConfig.State.ENABLED || statsRequest.isIncludeDisabled()).map(Indexer::getName).collect(Collectors.toSet()); - OptionalDouble overallAverage = resultList.stream().filter(x -> ((Object[]) x)[1] != null).mapToLong(x -> ((BigInteger) ((Object[]) x)[1]).longValue()).average(); + OptionalDouble overallAverage = resultList.stream().filter(x -> ((Object[]) x)[1] != null).mapToLong(x -> ((BigDecimal) ((Object[]) x)[1]).longValue()).average(); for (Object result : resultList) { Object[] resultSet = (Object[]) result; @@ -228,7 +255,7 @@ public class Stats { if (resultSet[0] == null || resultSet[1] == null || !indexerNamesToInclude.contains(indexerName)) { continue; } - long averageResponseTime = ((BigInteger) resultSet[1]).longValue(); + long averageResponseTime = ((BigDecimal) resultSet[1]).longValue(); averageResponseTimes.add(new AverageResponseTime(indexerName, averageResponseTime, averageResponseTime - overallAverage.orElse(0D))); } logger.debug(LoggingMarkers.PERFORMANCE, "Calculated average response times for indexers. Took {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); @@ -244,7 +271,7 @@ public class Stats { logger.debug("Calculating indexer result uniqueness scores"); List typesToUse = Arrays.asList(SearchModuleType.NEWZNAB, SearchModuleType.TORZNAB, SearchModuleType.ANIZB); - final Set indexersToInclude = (statsRequest.isIncludeDisabled() ? searchModuleProvider.getIndexers() : searchModuleProvider.getEnabledIndexers().stream().filter(x -> typesToUse.contains(x.getConfig().getSearchModuleType())).collect(Collectors.toList())).stream().map(Indexer::getName).collect(Collectors.toSet()); + final Set indexersToInclude = (statsRequest.isIncludeDisabled() ? searchModuleProvider.getIndexers() : searchModuleProvider.getEnabledIndexers().stream().filter(x -> typesToUse.contains(x.getConfig().getSearchModuleType())).toList()).stream().map(Indexer::getName).collect(Collectors.toSet()); List indexerUniquenessScores = calculateIndexerScores(indexersToInclude, uniquenessScoreEntityRepository.findAll()); logger.debug(LoggingMarkers.PERFORMANCE, "Calculated indexer result uniqueness scores. Took {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); @@ -306,7 +333,7 @@ public class Stats { if (!indexerIdsToInclude.contains(indexerId)) { continue; } - Double avg = (Double) array[1]; + Double avg = ((BigDecimal) array[1]).doubleValue(); accessesPerDayCountMap.put(indexerId, avg); } logger.debug(LoggingMarkers.PERFORMANCE, "Calculating accesses per day took {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); @@ -335,7 +362,7 @@ public class Stats { continue; } String result = (String) array[1]; - int count = ((BigInteger) array[2]).intValue(); + int count = ((Long) array[2]).intValue(); if (result.equals(IndexerAccessResult.SUCCESSFUL.name())) { successCountMap.put(indexerId, count); } else if (result.equals(IndexerAccessResult.CONNECTION_ERROR.name())) { @@ -402,7 +429,7 @@ public class Stats { //want 6 0 1 2 3 4 5 // S M T W T F S - BigInteger counter = (BigInteger) resultSet[1]; + Long counter = (Long) resultSet[1]; int indexInList = (index + 5) % 7; dayOfWeekCounts.get(indexInList).setCount(counter.intValue()); } @@ -430,7 +457,7 @@ public class Stats { for (Object o : resultList) { Object[] o2 = (Object[]) o; Integer index = (Integer) o2[0]; - BigInteger counter = (BigInteger) o2[1]; + Long counter = (Long) o2[1]; hourOfDayCounts.get(index).setCount(counter.intValue()); } @@ -486,17 +513,17 @@ public class Stats { if (!indexerNamesToInclude.contains(indexerName)) { continue; } - BigInteger countAll = (BigInteger) o2[1]; - BigInteger countSuccess = (BigInteger) o2[2]; - BigInteger countError = (BigInteger) o2[3]; + Long countAll = (Long) o2[1]; + Long countSuccess = (Long) o2[2]; + Long countError = (Long) o2[3]; if (countAll == null) { - countAll = BigInteger.ZERO; + countAll = 0L; } if (countSuccess == null) { - countSuccess = BigInteger.ZERO; + countSuccess = 0L; } if (countError == null) { - countError = BigInteger.ZERO; + countError = 0L; } Float percentSuccessful; @@ -536,8 +563,8 @@ public class Stats { for (Object o : resultList) { Object[] o2 = (Object[]) o; String usernameOrIp = (String) o2[0]; - int countForUser = ((BigInteger) o2[1]).intValue(); - float percentSuccessful = 100F / (((BigInteger) o2[2]).floatValue() / ((BigInteger) o2[1]).floatValue()); + int countForUser = ((Long) o2[1]).intValue(); + float percentSuccessful = 100F / (((Long) o2[2]).floatValue() / ((Long) o2[1]).floatValue()); result.add(new DownloadOrSearchSharePerUserOrIp(usernameOrIp, countForUser, percentSuccessful)); } result.sort(Comparator.comparingDouble(DownloadOrSearchSharePerUserOrIp::getPercentage).reversed()); @@ -563,7 +590,7 @@ public class Stats { for (Object o : resultList) { Object[] o2 = (Object[]) o; String userAgent = (String) o2[0]; - int countForUserAgent = ((BigInteger) o2[1]).intValue(); + int countForUserAgent = ((Long) o2[1]).intValue(); countAll += countForUserAgent; result.add(new UserAgentShare(userAgent, countForUserAgent)); } @@ -594,7 +621,7 @@ public class Stats { for (Object o : resultList) { Object[] o2 = (Object[]) o; String userAgent = (String) o2[0]; - int countForUserAgent = ((BigInteger) o2[1]).intValue(); + int countForUserAgent = ((Long) o2[1]).intValue(); countAll += countForUserAgent; result.add(new UserAgentShare(userAgent, countForUserAgent)); } @@ -610,15 +637,16 @@ public class Stats { List downloadsPerAge() { Stopwatch stopwatch = Stopwatch.createStarted(); logger.debug("Calculating downloads per age"); - String sql = "SELECT\n" + - " steps,\n" + - " count(*)\n" + - "FROM\n" + - " (SELECT age / 100 AS steps\n" + - " FROM INDEXERNZBDOWNLOAD\n" + - " WHERE age IS NOT NULL)\n" + - "GROUP BY steps\n" + - "ORDER BY steps ASC"; + String sql = """ + SELECT + steps, + count(*) + FROM + (SELECT age / 100 AS steps + FROM INDEXERNZBDOWNLOAD + WHERE age IS NOT NULL) + GROUP BY steps + ORDER BY steps ASC"""; Query query = entityManager.createNativeQuery(sql); List resultList = query.getResultList(); List results = new ArrayList<>(); @@ -626,7 +654,7 @@ public class Stats { for (Object o : resultList) { Object[] o2 = (Object[]) o; int ageStep = (Integer) o2[0]; - int count = ((BigInteger) o2[1]).intValue(); + int count = ((Long) o2[1]).intValue(); agesAndCountsMap.put(ageStep, count); } for (int i = 0; i <= 34; i += 1) { @@ -647,20 +675,22 @@ public class Stats { Stopwatch stopwatch = Stopwatch.createStarted(); logger.debug("Calculating downloads per age percentages"); DownloadPerAgeStats result = new DownloadPerAgeStats(); - String percentage = "SELECT CASE\n" + - " WHEN (SELECT CAST(COUNT(*) AS FLOAT) AS COUNT\n" + - " FROM INDEXERNZBDOWNLOAD\n" + - " WHERE AGE > %d) > 0\n" + - " THEN SELECT CAST(100 AS FLOAT) / (CAST(COUNT(i.*) AS FLOAT)/ x.COUNT)\n" + - "FROM INDEXERNZBDOWNLOAD i,\n" + - "( SELECT COUNT(*) AS COUNT\n" + - "FROM INDEXERNZBDOWNLOAD\n" + - "WHERE AGE > %d) AS x\n" + - "ELSE 0 END"; - result.setPercentOlder1000(((Double) entityManager.createNativeQuery(String.format(percentage, 1000, 1000)).getResultList().get(0)).intValue()); - result.setPercentOlder2000(((Double) entityManager.createNativeQuery(String.format(percentage, 2000, 2000)).getResultList().get(0)).intValue()); - result.setPercentOlder3000(((Double) entityManager.createNativeQuery(String.format(percentage, 3000, 3000)).getResultList().get(0)).intValue()); - result.setAverageAge((Integer) entityManager.createNativeQuery("SELECT AVG(AGE) FROM INDEXERNZBDOWNLOAD").getResultList().get(0)); + String percentage = """ + SELECT CASE + WHEN (SELECT CAST(COUNT(*) AS FLOAT) AS COUNT + FROM INDEXERNZBDOWNLOAD + WHERE AGE > %d) > 0 + THEN SELECT CAST(100 AS FLOAT) / (CAST(COUNT(i.*) AS FLOAT)/ x.COUNT) + FROM INDEXERNZBDOWNLOAD i, + ( SELECT COUNT(*) AS COUNT + FROM INDEXERNZBDOWNLOAD + WHERE AGE > %d) AS x + ELSE 0 END"""; + result.setPercentOlder1000(((BigDecimal) entityManager.createNativeQuery(String.format(percentage, 1000, 1000)).getResultList().get(0)).intValue()); + result.setPercentOlder2000(((BigDecimal) entityManager.createNativeQuery(String.format(percentage, 2000, 2000)).getResultList().get(0)).intValue()); + result.setPercentOlder3000(((BigDecimal) entityManager.createNativeQuery(String.format(percentage, 3000, 3000)).getResultList().get(0)).intValue()); + final Double averageAge = (Double) entityManager.createNativeQuery("SELECT AVG(AGE) FROM INDEXERNZBDOWNLOAD").getResultList().get(0); + result.setAverageAge(averageAge == null ? 0 : averageAge.intValue()); logger.debug(LoggingMarkers.PERFORMANCE, "Calculated downloads per age percentages . Took {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); result.setDownloadsPerAge(downloadsPerAge()); diff --git a/core/src/main/java/org/nzbhydra/historystats/StatsResponse.java b/core/src/main/java/org/nzbhydra/historystats/StatsResponse.java deleted file mode 100644 index 1294fa8df..000000000 --- a/core/src/main/java/org/nzbhydra/historystats/StatsResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.nzbhydra.historystats; - -import lombok.Data; -import org.nzbhydra.historystats.stats.*; - -import java.time.Instant; -import java.util.List; - -@Data -public class StatsResponse { - - private Instant after = null; - private Instant before = null; - - private List indexerApiAccessStats; - - private List indexerScores; - - private List avgResponseTimes; - - private List indexerDownloadShares; - - private List downloadsPerDayOfWeek; - private List downloadsPerHourOfDay; - - private List searchesPerDayOfWeek; - private List searchesPerHourOfDay; - - private DownloadPerAgeStats downloadsPerAgeStats; - private List successfulDownloadsPerIndexer; - private List downloadSharesPerUser; - private List downloadSharesPerIp; - private List searchSharesPerUser; - private List searchSharesPerIp; - - private List userAgentSearchShares; - private List userAgentDownloadShares; - - private int numberOfConfiguredIndexers; - private int numberOfEnabledIndexers; - -} diff --git a/core/src/main/java/org/nzbhydra/historystats/stats/CountPerDayOfWeek.java b/core/src/main/java/org/nzbhydra/historystats/stats/CountPerDayOfWeek.java deleted file mode 100644 index e8833ce05..000000000 --- a/core/src/main/java/org/nzbhydra/historystats/stats/CountPerDayOfWeek.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.nzbhydra.historystats.stats; - -import lombok.Data; - -import java.time.DayOfWeek; -import java.time.format.TextStyle; -import java.util.Locale; - -@Data -public class CountPerDayOfWeek { - private String day = null; - private Integer count = null; - - /** - * @param dayIndex 1 for "Mon", 2 for "Tue", etc. - */ - public CountPerDayOfWeek(int dayIndex, Integer counter) { - this.count = counter; - day = DayOfWeek.of(dayIndex).getDisplayName(TextStyle.SHORT, Locale.US); - } -} diff --git a/core/src/main/java/org/nzbhydra/indexers/Anizb.java b/core/src/main/java/org/nzbhydra/indexers/Anizb.java index bc93f1ec4..088077c8b 100644 --- a/core/src/main/java/org/nzbhydra/indexers/Anizb.java +++ b/core/src/main/java/org/nzbhydra/indexers/Anizb.java @@ -2,21 +2,30 @@ package org.nzbhydra.indexers; import com.google.common.base.Joiner; import joptsimple.internal.Strings; +import org.nzbhydra.config.BaseConfigHandler; +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.indexers.exceptions.IndexerParsingException; import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; +import org.nzbhydra.indexers.status.IndexerLimitRepository; import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; +import org.nzbhydra.mediainfo.InfoProvider; +import org.nzbhydra.searching.CategoryProvider; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; +import org.nzbhydra.searching.SearchResultAcceptor; import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult; +import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.HasNfo; import org.nzbhydra.searching.searchrequests.SearchRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; @@ -25,14 +34,16 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; -@Component public class Anizb extends Indexer { private static final Logger logger = LoggerFactory.getLogger(Anizb.class); + public Anizb(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, BaseConfigHandler baseConfigHandler) { + super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler); + } + @Override protected void completeIndexerSearchResult(NewznabXmlRoot response, IndexerSearchResult indexerSearchResult, AcceptorResult acceptorResult, SearchRequest searchRequest, int offset, Integer limit) { - indexerSearchResult.setHasMoreResults(false); indexerSearchResult.setTotalResults(indexerSearchResult.getSearchResultItems().size()); indexerSearchResult.setPageSize(100); @@ -107,7 +118,7 @@ public class Anizb extends Indexer { @Component @Order(2000) - public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { + public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { @Override public boolean handlesIndexerConfig(IndexerConfig config) { @@ -115,8 +126,10 @@ public class Anizb extends Indexer { } @Override - public Class getIndexerClass() { - return Anizb.class; + public String getName() { + return "ANIZB"; } + + } } diff --git a/core/src/main/java/org/nzbhydra/indexers/Binsearch.java b/core/src/main/java/org/nzbhydra/indexers/Binsearch.java index 5db31330a..f6dacff6b 100644 --- a/core/src/main/java/org/nzbhydra/indexers/Binsearch.java +++ b/core/src/main/java/org/nzbhydra/indexers/Binsearch.java @@ -2,33 +2,44 @@ package org.nzbhydra.indexers; import com.google.common.base.Joiner; import com.google.common.base.Throwables; +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; +import dev.failsafe.function.CheckedSupplier; import joptsimple.internal.Strings; -import net.jodah.failsafe.Failsafe; -import net.jodah.failsafe.RetryPolicy; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import org.nzbhydra.config.BaseConfigHandler; +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.indexers.exceptions.IndexerParsingException; import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; +import org.nzbhydra.indexers.status.IndexerLimitRepository; +import org.nzbhydra.mediainfo.InfoProvider; +import org.nzbhydra.searching.CategoryProvider; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; +import org.nzbhydra.searching.SearchResultAcceptor; import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult; +import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.HasNfo; import org.nzbhydra.searching.searchrequests.SearchRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -38,11 +49,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; -@Component public class Binsearch extends Indexer { private static final Logger logger = LoggerFactory.getLogger(Binsearch.class); @@ -56,7 +65,14 @@ public class Binsearch extends Indexer { private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder().appendPattern("dd-MMM-yyyy").parseDefaulting(ChronoField.NANO_OF_DAY, 0).toFormatter().withZone(ZoneId.of("UTC")).withLocale(Locale.ENGLISH); private static final Pattern NFO_PATTERN = Pattern.compile("
(?.*)<\\/pre>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
 
-    private final RetryPolicy retry503policy = new RetryPolicy().retryOn(x -> x instanceof IndexerAccessException && Throwables.getStackTraceAsString(x).contains("503")).withDelay(500, TimeUnit.MILLISECONDS).withMaxRetries(2);
+    private final RetryPolicy retry503policy = RetryPolicy.builder()
+        .handleIf(x -> x instanceof IndexerAccessException && Throwables.getStackTraceAsString(x).contains("503"))
+        .withDelay(Duration.ofMillis(500))
+        .withMaxRetries(2).build();
+
+    public Binsearch(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, BaseConfigHandler baseConfigHandler) {
+        super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler);
+    }
 
 
     //LATER It's not ideal that currently the web response needs to be parsed twice, once for the search results and once for the completion of the indexer search result. Will need to check how much that impacts performance
@@ -88,7 +104,6 @@ public class Binsearch extends Indexer {
     }
 
 
-    @SuppressWarnings("ConstantConditions")
     @Override
     protected List getSearchResultItems(String searchRequestResponse, SearchRequest searchRequest) throws IndexerParsingException {
         List items = new ArrayList<>();
@@ -154,28 +169,18 @@ public class Binsearch extends Indexer {
         Matcher posterMatcher = POSTER_PATTERN.matcher(collectionLink); //e.g. Ramer%40marmer.com+%28Clown_nez%29
         if (posterMatcher.find()) {
             String poster = posterMatcher.group(1).trim();
-            try {
-                poster = URLDecoder.decode(poster, "UTF-8").replace("+", " ");
-                item.setPoster(poster);
-            } catch (UnsupportedEncodingException e) {
-                debug("Unable to decode poster {}", poster);
-            }
+            poster = URLDecoder.decode(poster, StandardCharsets.UTF_8).replace("+", " ");
+            item.setPoster(poster);
         }
 
         Matcher sizeMatcher = SIZE_PATTERN.matcher(infoElement.ownText());
         if (sizeMatcher.find()) {
-            Float size = Float.valueOf(sizeMatcher.group("size"));
+            Float size = Float.parseFloat(sizeMatcher.group("size"));
             String unit = sizeMatcher.group("unit");
             switch (unit) {
-                case "GB":
-                    size = size * 1000 * 1000 * 1000;
-                    break;
-                case "MB":
-                    size = size * 1000 * 1000;
-                    break;
-                case "KB":
-                    size = size * 1000;
-                    break;
+                case "GB" -> size = size * 1000 * 1000 * 1000;
+                case "MB" -> size = size * 1000 * 1000;
+                case "KB" -> size = size * 1000;
             }
             item.setSize(size.longValue());
         } else {
@@ -250,8 +255,13 @@ public class Binsearch extends Indexer {
     @Override
     protected String getAndStoreResultToDatabase(URI uri, IndexerApiAccessType apiAccessType) throws IndexerAccessException {
         return Failsafe.with(retry503policy)
-                .onFailedAttempt(throwable -> logger.warn("Encountered 503 error. Will retry"))
-                .get(() -> getAndStoreResultToDatabase(uri, String.class, apiAccessType));
+            .onFailure(throwable -> logger.warn("Encountered 503 error. Will retry"))
+            .get(new CheckedSupplier<>() {
+                @Override
+                public String get() throws Throwable {
+                    return getAndStoreResultToDatabase(uri, String.class, apiAccessType);
+                }
+            });
     }
 
     @Override
@@ -261,7 +271,7 @@ public class Binsearch extends Indexer {
 
     @Component
     @Order(2000)
-    public static class NewznabHandlingStrategy implements IndexerHandlingStrategy {
+    public static class NewznabHandlingStrategy implements IndexerHandlingStrategy {
 
         @Override
         public boolean handlesIndexerConfig(IndexerConfig config) {
@@ -269,8 +279,8 @@ public class Binsearch extends Indexer {
         }
 
         @Override
-        public Class getIndexerClass() {
-            return Binsearch.class;
+        public String getName() {
+            return "BINSEARCH";
         }
     }
 }
diff --git a/core/src/main/java/org/nzbhydra/indexers/DevIndexer.java b/core/src/main/java/org/nzbhydra/indexers/DevIndexer.java
index ebf5fa960..80405cd74 100644
--- a/core/src/main/java/org/nzbhydra/indexers/DevIndexer.java
+++ b/core/src/main/java/org/nzbhydra/indexers/DevIndexer.java
@@ -3,17 +3,27 @@ package org.nzbhydra.indexers;
 import lombok.Getter;
 import lombok.Setter;
 import org.nzbhydra.NzbHydraException;
+import org.nzbhydra.config.BaseConfigHandler;
+import org.nzbhydra.config.ConfigProvider;
 import org.nzbhydra.config.indexer.IndexerConfig;
 import org.nzbhydra.config.indexer.SearchModuleType;
 import org.nzbhydra.indexers.exceptions.IndexerAccessException;
+import org.nzbhydra.indexers.status.IndexerLimitRepository;
 import org.nzbhydra.mapping.newznab.mock.NewznabMockBuilder;
 import org.nzbhydra.mapping.newznab.mock.NewznabMockRequest;
 import org.nzbhydra.mapping.newznab.xml.NewznabAttribute;
 import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem;
 import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot;
 import org.nzbhydra.mapping.newznab.xml.Xml;
+import org.nzbhydra.mediainfo.InfoProvider;
+import org.nzbhydra.searching.CategoryProvider;
+import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler;
+import org.nzbhydra.searching.SearchResultAcceptor;
+import org.nzbhydra.searching.db.SearchResultRepository;
 import org.nzbhydra.searching.dtoseventsenums.SearchResultItem;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.core.annotation.Order;
+import org.springframework.oxm.Unmarshaller;
 import org.springframework.stereotype.Component;
 
 import java.net.URI;
@@ -25,6 +35,10 @@ import java.util.Collections;
 @Component
 public class DevIndexer extends Newznab {
 
+    public DevIndexer(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, Unmarshaller unmarshaller, BaseConfigHandler baseConfigHandler) {
+        super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler);
+    }
+
     protected Xml getAndStoreResultToDatabase(URI uri, IndexerApiAccessType apiAccessType) throws IndexerAccessException {
 
 
@@ -98,7 +112,7 @@ public class DevIndexer extends Newznab {
 
     @Component
     @Order(500)
-    public static class DevIndexerHandlingStrategy implements IndexerHandlingStrategy {
+    public static class DevIndexerHandlingStrategy implements IndexerHandlingStrategy {
 
         @Override
         public boolean handlesIndexerConfig(IndexerConfig config) {
@@ -106,8 +120,8 @@ public class DevIndexer extends Newznab {
         }
 
         @Override
-        public Class getIndexerClass() {
-            return DevIndexer.class;
+        public String getName() {
+            return "DEVONLY";
         }
     }
 
diff --git a/core/src/main/java/org/nzbhydra/indexers/DogNzb.java b/core/src/main/java/org/nzbhydra/indexers/DogNzb.java
index c06ce554b..c3da1b901 100644
--- a/core/src/main/java/org/nzbhydra/indexers/DogNzb.java
+++ b/core/src/main/java/org/nzbhydra/indexers/DogNzb.java
@@ -1,16 +1,26 @@
 package org.nzbhydra.indexers;
 
+import org.nzbhydra.config.BaseConfigHandler;
+import org.nzbhydra.config.ConfigProvider;
 import org.nzbhydra.config.indexer.IndexerConfig;
 import org.nzbhydra.config.indexer.SearchModuleType;
+import org.nzbhydra.indexers.status.IndexerLimitRepository;
 import org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse;
 import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot;
 import org.nzbhydra.mapping.newznab.xml.Xml;
+import org.nzbhydra.mediainfo.InfoProvider;
+import org.nzbhydra.searching.CategoryProvider;
+import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler;
+import org.nzbhydra.searching.SearchResultAcceptor;
 import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult;
+import org.nzbhydra.searching.db.SearchResultRepository;
 import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult;
 import org.nzbhydra.searching.searchrequests.SearchRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.core.annotation.Order;
+import org.springframework.oxm.Unmarshaller;
 import org.springframework.stereotype.Component;
 
 @Component
@@ -18,6 +28,10 @@ public class DogNzb extends Newznab {
 
     private static final Logger logger = LoggerFactory.getLogger(DogNzb.class);
 
+    public DogNzb(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, Unmarshaller unmarshaller, BaseConfigHandler baseConfigHandler) {
+        super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler);
+    }
+
     protected void completeIndexerSearchResult(Xml response, IndexerSearchResult indexerSearchResult, AcceptorResult acceptorResult, SearchRequest searchRequest, int offset, Integer limit) {
         NewznabXmlResponse newznabResponse = ((NewznabXmlRoot) response).getRssChannel().getNewznabResponse();
         super.completeIndexerSearchResult(response, indexerSearchResult, acceptorResult, searchRequest, offset, limit);
@@ -46,7 +60,7 @@ public class DogNzb extends Newznab {
 
     @Component
     @Order(100)
-    public static class NewznabHandlingStrategy implements IndexerHandlingStrategy {
+    public static class NewznabHandlingStrategy implements IndexerHandlingStrategy {
 
         @Override
         public boolean handlesIndexerConfig(IndexerConfig config) {
@@ -58,9 +72,11 @@ public class DogNzb extends Newznab {
         }
 
         @Override
-        public Class getIndexerClass() {
-            return DogNzb.class;
+        public String getName() {
+            return "DOGNZB";
         }
+
+
     }
 
 }
diff --git a/core/src/main/java/org/nzbhydra/indexers/Indexer.java b/core/src/main/java/org/nzbhydra/indexers/Indexer.java
index 4e9bdc102..5bc1a2482 100644
--- a/core/src/main/java/org/nzbhydra/indexers/Indexer.java
+++ b/core/src/main/java/org/nzbhydra/indexers/Indexer.java
@@ -2,7 +2,9 @@ package org.nzbhydra.indexers;
 
 import com.google.common.base.Objects;
 import com.google.common.base.Stopwatch;
+import jakarta.persistence.EntityExistsException;
 import joptsimple.internal.Strings;
+import org.nzbhydra.config.BaseConfigHandler;
 import org.nzbhydra.config.ConfigChangedEvent;
 import org.nzbhydra.config.ConfigProvider;
 import org.nzbhydra.config.indexer.IndexerConfig;
@@ -19,7 +21,7 @@ import org.nzbhydra.mediainfo.InfoProvider;
 import org.nzbhydra.notifications.IndexerDisabledNotificationEvent;
 import org.nzbhydra.notifications.IndexerReenabledNotificationEvent;
 import org.nzbhydra.searching.CategoryProvider;
-import org.nzbhydra.searching.CustomQueryAndTitleMapping;
+import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler;
 import org.nzbhydra.searching.SearchResultAcceptor;
 import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult;
 import org.nzbhydra.searching.SearchResultIdCalculator;
@@ -35,14 +37,13 @@ import org.nzbhydra.searching.searchrequests.SearchRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.Marker;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.aot.hint.annotation.Reflective;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.event.EventListener;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.util.UriComponentsBuilder;
 
-import javax.persistence.EntityExistsException;
 import java.net.URI;
 import java.time.Instant;
 import java.time.format.DateTimeFormatter;
@@ -61,15 +62,10 @@ import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 @SuppressWarnings("TypeParameterHidesVisibleType")
+@Reflective
 @Component
 public abstract class Indexer {
 
-    public enum BackendType {
-        NZEDB,
-        NNTMUX,
-        NEWZNAB
-    }
-
     protected static final List DISABLE_PERIODS = Arrays.asList(0, 5, 15, 30, 60, 3 * 60);
     private static final Logger logger = LoggerFactory.getLogger(Indexer.class);
 
@@ -81,37 +77,60 @@ public abstract class Indexer {
     protected IndexerConfig config;
     private Pattern cleanupPattern;
 
-    @Autowired
-    protected ConfigProvider configProvider;
-    @Autowired
-    protected IndexerRepository indexerRepository;
-    @Autowired
-    protected SearchResultRepository searchResultRepository;
-    @Autowired
-    protected IndexerApiAccessRepository indexerApiAccessRepository;
-    @Autowired
-    protected IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository;
-    @Autowired
-    private IndexerLimitRepository indexerStatusRepository;
-    @Autowired
-    protected IndexerWebAccess indexerWebAccess;
-    @Autowired
-    protected SearchResultAcceptor resultAcceptor;
-    @Autowired
-    protected CategoryProvider categoryProvider;
-    @Autowired
-    protected InfoProvider infoProvider;
-    @Autowired
-    private ApplicationEventPublisher eventPublisher;
-    @Autowired
-    private QueryGenerator queryGenerator;
-    @Autowired
-    private CustomQueryAndTitleMapping titleMapping;
 
+    protected ConfigProvider configProvider;
+
+    protected IndexerRepository indexerRepository;
+
+    protected SearchResultRepository searchResultRepository;
+
+    protected IndexerApiAccessRepository indexerApiAccessRepository;
+
+    protected IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository;
+
+
+    protected IndexerWebAccess indexerWebAccess;
+
+    protected SearchResultAcceptor resultAcceptor;
+
+    protected CategoryProvider categoryProvider;
+
+    protected InfoProvider infoProvider;
+
+    private ApplicationEventPublisher eventPublisher;
+
+    private QueryGenerator queryGenerator;
+
+    private CustomQueryAndTitleMappingHandler titleMapping;
+
+    private BaseConfigHandler baseConfigHandler;
+
+
+    protected Indexer() {
+    }
+
+    public Indexer(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, BaseConfigHandler baseConfigHandler) {
+        this.configProvider = configProvider;
+        this.indexerRepository = indexerRepository;
+        this.searchResultRepository = searchResultRepository;
+        this.indexerApiAccessRepository = indexerApiAccessRepository;
+        this.indexerApiAccessShortRepository = indexerApiAccessShortRepository;
+        this.indexerWebAccess = indexerWebAccess;
+        this.resultAcceptor = resultAcceptor;
+        this.categoryProvider = categoryProvider;
+        this.infoProvider = infoProvider;
+        this.eventPublisher = eventPublisher;
+        this.queryGenerator = queryGenerator;
+        this.titleMapping = titleMapping;
+        this.baseConfigHandler = baseConfigHandler;
+    }
 
     public void initialize(IndexerConfig config, IndexerEntity indexer) {
         this.indexer = indexer;
         this.config = config;
+        if (queryGenerator == null) {
+            logger.error("Indexer {} not properly initialized. No beans autowired.", config.getName());
+        }
     }
 
     @EventListener
@@ -174,7 +193,10 @@ public abstract class Indexer {
 
     private boolean isFallbackRequired(SearchRequest searchRequest, IndexerSearchResult indexerSearchResult) {
         final FallbackState fallbackStateByIndexer = searchRequest.getInternalData().getFallbackStateByIndexer(getName());
-        return indexerSearchResult.getTotalResults() == 0 && !searchRequest.getIdentifiers().isEmpty() && fallbackStateByIndexer != FallbackState.USED && configProvider.getBaseConfig().getSearching().getIdFallbackToQueryGeneration().meets(searchRequest);
+        if (indexerSearchResult.getTotalResults() != 0 || searchRequest.getIdentifiers().isEmpty() || fallbackStateByIndexer == FallbackState.USED) {
+            return false;
+        }
+        return searchRequest.meets(configProvider.getBaseConfig().getSearching().getIdFallbackToQueryGeneration());
     }
 
     protected IndexerSearchResult searchInternal(SearchRequest searchRequest, int offset, Integer limit) throws IndexerSearchAbortedException, IndexerAccessException {
@@ -299,7 +321,7 @@ public abstract class Indexer {
         getConfig().setDisabledUntil(null);
         getConfig().setDisabledLevel(0);
         getConfig().setDisabledAt(null);
-        configProvider.getBaseConfig().save(false);
+        baseConfigHandler.save(false);
         saveApiAccess(accessType, responseTime, IndexerAccessResult.SUCCESSFUL, true);
     }
 
@@ -317,7 +339,7 @@ public abstract class Indexer {
         }
         getConfig().setLastError(reason);
         getConfig().setDisabledAt(Instant.now());
-        configProvider.getBaseConfig().save(false);
+        baseConfigHandler.save(false);
         eventPublisher.publishEvent(new IndexerDisabledNotificationEvent(indexer.getName(), getConfig().getState(), reason));
 
         saveApiAccess(accessType, responseTime, accessResult, false);
@@ -415,7 +437,7 @@ public abstract class Indexer {
         }
         title = title.trim().replace("&", "");
 
-        List removeTrailing = configProvider.getBaseConfig().getSearching().getRemoveTrailing().stream().map(x -> x.toLowerCase().trim()).collect(Collectors.toList());
+        List removeTrailing = configProvider.getBaseConfig().getSearching().getRemoveTrailing().stream().map(x -> x.toLowerCase().trim()).toList();
         if (removeTrailing.isEmpty()) {
             return title;
         }
@@ -463,7 +485,7 @@ public abstract class Indexer {
 
     @Override
     public int hashCode() {
-        return (config == null || config.getName() == null) ? 0 : config.getName().hashCode();
+        return (getConfig() == null || getConfig().getName() == null) ? 0 : config.getName().hashCode();
     }
 
     @Override
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntity.java b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntity.java
index e9997b8e8..4fe6809f7 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntity.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntity.java
@@ -1,27 +1,40 @@
 package org.nzbhydra.indexers;
 
 import com.google.common.base.MoreObjects;
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.hibernate.annotations.OnDelete;
 import org.hibernate.annotations.OnDeleteAction;
+import org.nzbhydra.springnative.ReflectionMarker;
 
-import javax.persistence.*;
 import java.time.Instant;
 import java.util.Objects;
 
 
 @Data
+@ReflectionMarker
 @Entity
 @NoArgsConstructor
 @Table(name = "indexerapiaccess")
-public class IndexerApiAccessEntity {
+public final class IndexerApiAccessEntity {
 
     @Id
+    @SequenceGenerator(allocationSize = 1, name = "INDEXERAPIACCESS_SEQ")
     @GeneratedValue(strategy = GenerationType.SEQUENCE)
     protected int id;
 
-    @ManyToOne(fetch = FetchType.LAZY)
+    @ManyToOne
     @OnDelete(action = OnDeleteAction.CASCADE)
     private IndexerEntity indexer;
 
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntityShort.java b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntityShort.java
index d44e31f55..4c50e7de1 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntityShort.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessEntityShort.java
@@ -1,9 +1,19 @@
 package org.nzbhydra.indexers;
 
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.nzbhydra.springnative.ReflectionMarker;
 
-import javax.persistence.*;
 import java.time.Instant;
 
 
@@ -12,13 +22,15 @@ import java.time.Instant;
  * checking for API hit limits much faster
  */
 @Data
+@ReflectionMarker
 @Entity
 @NoArgsConstructor
 @Table(name = "indexerapiaccess_short")
-public class IndexerApiAccessEntityShort {
+public final class IndexerApiAccessEntityShort {
 
     @Id
     @GeneratedValue(strategy = GenerationType.SEQUENCE)
+    @SequenceGenerator(allocationSize = 1, name = "INDEXERAPIACCESS_SHORT_SEQ")
     protected int id;
 
     @Column(name = "INDEXER_ID")
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessRepository.java b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessRepository.java
index 58e078ae1..2c2d89a16 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessRepository.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessRepository.java
@@ -1,19 +1,10 @@
 package org.nzbhydra.indexers;
 
 
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
 
-import java.util.Collection;
-
+//Only used for stats, do not delete just because there's usages of reading methods!
 public interface IndexerApiAccessRepository extends JpaRepository {
 
-    IndexerApiAccessEntity findByIndexer(IndexerEntity indexerEntity);
-
-    Page findByIndexerOrderByTimeDesc(IndexerEntity indexerEntity, Pageable pageable);
-
-    void deleteAllByIndexerIn(Collection searchEntity);
-
 
 }
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerEntity.java b/core/src/main/java/org/nzbhydra/indexers/IndexerEntity.java
index cf5ac2015..726083824 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerEntity.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerEntity.java
@@ -1,20 +1,29 @@
 package org.nzbhydra.indexers;
 
 import com.google.common.base.MoreObjects;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
 import lombok.Data;
+import org.nzbhydra.springnative.ReflectionMarker;
 
-import javax.persistence.*;
 import java.util.Objects;
 
 
 @Data
+@ReflectionMarker
 @Entity
 @Table(name = "indexer")
-public class IndexerEntity {
+public final class IndexerEntity {
 
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
-    protected int id;
+    @SequenceGenerator(allocationSize = 1, name = "INDEXER_SEQ")
+    private int id;
 
     @Column(unique = true)
     private String name;
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerHandlingStrategy.java b/core/src/main/java/org/nzbhydra/indexers/IndexerHandlingStrategy.java
index e6a7c43ed..84fca6de3 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerHandlingStrategy.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerHandlingStrategy.java
@@ -2,10 +2,10 @@ package org.nzbhydra.indexers;
 
 import org.nzbhydra.config.indexer.IndexerConfig;
 
-public interface IndexerHandlingStrategy {
+public interface IndexerHandlingStrategy {
 
     boolean handlesIndexerConfig(IndexerConfig config);
 
-    Class getIndexerClass();
+    String getName();
 
 }
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerSearchEntity.java b/core/src/main/java/org/nzbhydra/indexers/IndexerSearchEntity.java
index 6789e233f..f5d0fdec8 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerSearchEntity.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerSearchEntity.java
@@ -1,22 +1,31 @@
 package org.nzbhydra.indexers;
 
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.SequenceGenerator;
+import jakarta.persistence.Table;
 import lombok.Data;
 import org.hibernate.annotations.OnDelete;
 import org.hibernate.annotations.OnDeleteAction;
 import org.nzbhydra.searching.db.SearchEntity;
+import org.nzbhydra.springnative.ReflectionMarker;
 
-import javax.persistence.*;
 import java.util.Objects;
-import java.util.Random;
 
 
 @Data
+@ReflectionMarker
 @Entity
 @Table(name = "indexersearch", indexes = {@Index(name = "ISINDEX1", columnList = "INDEXER_ENTITY_ID"), @Index(name = "ISINDEX2", columnList = "SEARCH_ENTITY_ID")})
-public class IndexerSearchEntity {
+public final class IndexerSearchEntity {
 
     @Id
     @GeneratedValue(strategy = GenerationType.SEQUENCE)
+    @SequenceGenerator(allocationSize = 1, name = "INDEXERSEARCH_SEQ")
     private int id;
 
     @ManyToOne
@@ -36,10 +45,10 @@ public class IndexerSearchEntity {
     public IndexerSearchEntity() {
     }
 
-    public IndexerSearchEntity(IndexerEntity indexerEntity, SearchEntity searchEntity) {
+    public IndexerSearchEntity(IndexerEntity indexerEntity, SearchEntity searchEntity, int id) {
         this.indexerEntity = indexerEntity;
         this.searchEntity = searchEntity;
-        this.id = new Random().nextInt();
+        this.id = id;
     }
 
     @Override
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerStatusesCleanupTask.java b/core/src/main/java/org/nzbhydra/indexers/IndexerStatusesCleanupTask.java
index 7acea6c3c..1636d1701 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerStatusesCleanupTask.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerStatusesCleanupTask.java
@@ -16,6 +16,7 @@
 
 package org.nzbhydra.indexers;
 
+import org.nzbhydra.config.BaseConfigHandler;
 import org.nzbhydra.config.ConfigProvider;
 import org.nzbhydra.config.ConfigReaderWriter;
 import org.nzbhydra.config.indexer.IndexerConfig;
@@ -35,10 +36,12 @@ public class IndexerStatusesCleanupTask {
 
     private ConfigProvider configProvider;
     ConfigReaderWriter configReaderWriter = new ConfigReaderWriter();
+    private BaseConfigHandler baseConfigHandler;
 
     @Autowired
-    public IndexerStatusesCleanupTask(ConfigProvider configProvider) {
+    public IndexerStatusesCleanupTask(ConfigProvider configProvider, BaseConfigHandler baseConfigHandler) {
         this.configProvider = configProvider;
+        this.baseConfigHandler = baseConfigHandler;
     }
 
     @HydraTask(configId = "cleanUpIndexerStatuses", name = "Clean up indexer statuses", interval = MINUTE)
@@ -56,7 +59,7 @@ public class IndexerStatusesCleanupTask {
             }
         }
         if (anyChanges) {
-            configProvider.getBaseConfig().save(false);
+            baseConfigHandler.save(false);
         }
     }
 }
diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerWebAccess.java b/core/src/main/java/org/nzbhydra/indexers/IndexerWebAccess.java
index 08cfd3ce4..06f468058 100644
--- a/core/src/main/java/org/nzbhydra/indexers/IndexerWebAccess.java
+++ b/core/src/main/java/org/nzbhydra/indexers/IndexerWebAccess.java
@@ -4,12 +4,14 @@ import com.google.common.base.Throwables;
 import com.google.common.io.BaseEncoding;
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import org.nzbhydra.config.ConfigProvider;
 import org.nzbhydra.config.indexer.IndexerConfig;
 import org.nzbhydra.indexers.exceptions.IndexerAccessException;
 import org.nzbhydra.indexers.exceptions.IndexerProgramErrorException;
 import org.nzbhydra.indexers.exceptions.IndexerUnreachableException;
 import org.nzbhydra.logging.MdcThreadPoolExecutor;
+import org.nzbhydra.springnative.ReflectionMarker;
 import org.nzbhydra.web.WebConfiguration;
 import org.nzbhydra.webaccess.WebAccess;
 import org.slf4j.Logger;
@@ -47,7 +49,6 @@ public class IndexerWebAccess {
     protected Unmarshaller unmarshaller = new WebConfiguration().marshaller();
 
 
-    @SuppressWarnings("unchecked")
     public  T get(URI uri, IndexerConfig indexerConfig) throws IndexerAccessException {
         return get(uri, indexerConfig, null);
     }
@@ -73,8 +74,11 @@ public class IndexerWebAccess {
                     return (T) response;
                 }
                 try {
-                    T unmarshalled = (T) unmarshaller.unmarshal(new StreamSource(new StringReader(response)));
-                    return unmarshalled;
+                    try (StringReader reader = new StringReader(response)) {
+                        final StreamSource source = new StreamSource(reader);
+                        T unmarshalled = (T) unmarshaller.unmarshal(source);
+                        return unmarshalled;
+                    }
                 } catch (UnmarshallingFailureException e) {
                     if (!response.toLowerCase().contains("function not available")) {
                         //Some indexers like Animetosho don't return a proper error code. This error may happen during caps check and we don't want to log it
@@ -98,7 +102,7 @@ public class IndexerWebAccess {
             if (e.getCause() instanceof HydraUnmarshallingFailureException) {
                 throw new IndexerAccessException("Unable to parse indexer output", e.getCause());
             }
-
+            logger.debug("Indexer communication error", e.getCause());
             throw new IndexerUnreachableException("Error while communicating with indexer " + indexerConfig.getName() + ". Server returned: " + e.getMessage(), e.getCause());
         } catch (TimeoutException e) {
             throw new IndexerUnreachableException("Indexer did not complete request within " + timeout + " seconds");
@@ -118,10 +122,14 @@ public class IndexerWebAccess {
             int to = Math.min(lines.length, lineNumber + 5);
             String excerpt = String.join("\r\n", Arrays.asList(lines).subList(from, to));
             logger.error("Unable to parse indexer output at line {} and column {} with error message: {}. Excerpt:\r\n{}", lineNumber, columnNumber, message, excerpt);
+        } else {
+            logger.debug("Unable to parse indexer output:\n {}", response, e);
         }
     }
 
+    @EqualsAndHashCode(callSuper = true)
     @Data
+@ReflectionMarker
     @AllArgsConstructor
     public static class HydraUnmarshallingFailureException extends Exception {
         private org.springframework.oxm.UnmarshallingFailureException unmarshallingFailureException;
diff --git a/core/src/main/java/org/nzbhydra/indexers/Newznab.java b/core/src/main/java/org/nzbhydra/indexers/Newznab.java
index e559a274b..4ed124bee 100644
--- a/core/src/main/java/org/nzbhydra/indexers/Newznab.java
+++ b/core/src/main/java/org/nzbhydra/indexers/Newznab.java
@@ -5,12 +5,18 @@ import com.google.common.base.Strings;
 import lombok.Getter;
 import lombok.Setter;
 import org.nzbhydra.NzbHydraException;
+import org.nzbhydra.config.BaseConfigHandler;
+import org.nzbhydra.config.ConfigProvider;
 import org.nzbhydra.config.category.CategoriesConfig;
 import org.nzbhydra.config.category.Category;
 import org.nzbhydra.config.category.Category.Subtype;
+import org.nzbhydra.config.downloading.DownloadType;
+import org.nzbhydra.config.indexer.BackendType;
 import org.nzbhydra.config.indexer.IndexerCategoryConfig;
 import org.nzbhydra.config.indexer.IndexerConfig;
 import org.nzbhydra.config.indexer.SearchModuleType;
+import org.nzbhydra.config.mediainfo.MediaIdType;
+import org.nzbhydra.config.searching.SearchType;
 import org.nzbhydra.indexers.exceptions.IndexerAccessException;
 import org.nzbhydra.indexers.exceptions.IndexerAuthException;
 import org.nzbhydra.indexers.exceptions.IndexerErrorCodeException;
@@ -30,22 +36,25 @@ import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem;
 import org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse;
 import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot;
 import org.nzbhydra.mapping.newznab.xml.Xml;
+import org.nzbhydra.mediainfo.InfoProvider;
 import org.nzbhydra.mediainfo.InfoProviderException;
-import org.nzbhydra.mediainfo.MediaIdType;
 import org.nzbhydra.mediainfo.MediaInfo;
+import org.nzbhydra.searching.CategoryProvider;
+import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler;
+import org.nzbhydra.searching.SearchResultAcceptor;
 import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult;
 import org.nzbhydra.searching.SearchResultIdCalculator;
 import org.nzbhydra.searching.UnknownResponseException;
+import org.nzbhydra.searching.db.SearchResultRepository;
 import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult;
 import org.nzbhydra.searching.dtoseventsenums.SearchResultItem;
-import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType;
 import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.HasNfo;
-import org.nzbhydra.searching.dtoseventsenums.SearchType;
 import org.nzbhydra.searching.searchrequests.InternalData;
 import org.nzbhydra.searching.searchrequests.SearchRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.core.annotation.Order;
 import org.springframework.oxm.Unmarshaller;
 import org.springframework.stereotype.Component;
@@ -74,7 +83,6 @@ import java.util.stream.Stream;
 
 @Getter
 @Setter
-@Component
 public class Newznab extends Indexer {
 
     private static final Logger logger = LoggerFactory.getLogger(Newznab.class);
@@ -83,8 +91,6 @@ public class Newznab extends Indexer {
 
     private static final List LANGUAGES = Arrays.asList(" English", " Korean", " Spanish", " French", " German", " Italian", " Danish", " Dutch", " Japanese", " Cantonese", " Mandarin", " Russian", " Polish", " Vietnamese", " Swedish", " Norwegian", " Finnish", " Turkish", " Portuguese", " Flemish", " Greek", " Hungarian");
     private static Pattern GROUP_PATTERN = Pattern.compile(".*Group:<\\/b> ?([\\w\\.]+)
.*"); - private static Pattern GUID_PATTERN = Pattern.compile("(.*\\/)?([a-zA-Z0-9@\\.]+)(#\\w+)?"); - private static final List HOSTS_NOT_SUPPORTING_EMPTY_TYPE_SEARCH = Arrays.asList("nzbgeek", "6box"); private static final List HOSTS_NOT_SUPPORTING_SPECIAL_TYPE_Q_SEARCH = Arrays.asList("dognzb", "nzbplanet", "nzbgeek", "6box"); @@ -105,8 +111,14 @@ public class Newznab extends Indexer { private Unmarshaller unmarshaller; @Autowired private IndexerLimitRepository indexerStatusRepository; - private ConcurrentHashMap idToCategory = new ConcurrentHashMap<>(); + private final ConcurrentHashMap idToCategory = new ConcurrentHashMap<>(); + + public Newznab(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, Unmarshaller unmarshaller, BaseConfigHandler baseConfigHandler) { + super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler); + this.unmarshaller = unmarshaller; + this.indexerStatusRepository = indexerStatusRepository; + } protected UriComponentsBuilder getBaseUri() { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(config.getHost()).path(config.getApiPath().orElse("/api")); @@ -125,7 +137,7 @@ public class Newznab extends Indexer { } boolean searchTypeTvOrMovie = searchRequest.getSearchType() == SearchType.MOVIE || searchRequest.getSearchType() == SearchType.TVSEARCH; if (searchTypeTvOrMovie && searchRequest.getIdentifiers().isEmpty()) { - if (!searchRequest.getQuery().isPresent() && isIndexerNotSupportingEmptyTypeSearch()) { + if (searchRequest.getQuery().isEmpty() && isIndexerNotSupportingEmptyTypeSearch()) { debug("Switching search type to SEARCH because this indexer doesn't allow using search type MOVIE/TVSEARCH without identifiers and without query"); searchType = SearchType.SEARCH; } else if (searchRequest.getQuery().isPresent() && isIndexerNotSupportingSpecialTypeQSearch()) { @@ -370,7 +382,7 @@ public class Newznab extends Indexer { debug("Indexer doesn't support any of the provided search IDs: {}", Joiner.on(", ").join(searchRequest.getIdentifiers().keySet())); return true; } - if (configProvider.getBaseConfig().getSearching().getAlwaysConvertIds().meets(searchRequest.getSource())) { + if (searchRequest.getSource().meets(configProvider.getBaseConfig().getSearching().getAlwaysConvertIds())) { debug("Will convert IDs as ID conversion is to be always done for {}", configProvider.getBaseConfig().getSearching().getAlwaysConvertIds()); return true; } @@ -538,16 +550,23 @@ public class Newznab extends Indexer { String link = getEnclosureUrl(item); searchResultItem.setLink(link); + String guid = item.getRssGuid().getGuid(); if (item.getRssGuid().isPermaLink()) { - searchResultItem.setDetails(item.getRssGuid().getGuid()); - Matcher matcher = GUID_PATTERN.matcher(item.getRssGuid().getGuid()); - if (matcher.matches()) { - searchResultItem.setIndexerGuid(matcher.group(2)); + searchResultItem.setDetails(guid); + int index = guid.lastIndexOf("id="); + if (index > -1) { + guid = guid.substring(index + 3); } else { - searchResultItem.setIndexerGuid(item.getRssGuid().getGuid()); + index = guid.lastIndexOf("/"); + guid = guid.substring(index + 1); + index = guid.indexOf("#"); + if (index > -1) { + guid = guid.substring(0, index); + } } + searchResultItem.setIndexerGuid(guid); } else { - searchResultItem.setIndexerGuid(item.getRssGuid().getGuid()); + searchResultItem.setIndexerGuid(guid); } if (!Strings.isNullOrEmpty(item.getComments()) && Strings.isNullOrEmpty(searchResultItem.getDetails())) { @@ -580,7 +599,7 @@ public class Newznab extends Indexer { link = item.getEnclosures().get(0).getUrl(); } else { Optional nzbEnclosure = item.getEnclosures().stream().filter(x -> getEnclosureType().equals(x.getType())).findAny(); - if (!nzbEnclosure.isPresent()) { + if (nzbEnclosure.isEmpty()) { warn("Unable to find URL for result " + item.getTitle() + ". Will skip it."); throw new NzbHydraException(); } @@ -654,7 +673,7 @@ public class Newznab extends Indexer { //For these backends if not specified it doesn't exist searchResultItem.setHasNfo(HasNfo.NO); } - if (!searchResultItem.getGroup().isPresent() && !Strings.isNullOrEmpty(item.getDescription()) && item.getDescription().contains("Group:")) { + if (searchResultItem.getGroup().isEmpty() && !Strings.isNullOrEmpty(item.getDescription()) && item.getDescription().contains("Group:")) { //Dog has the group in the description, perhaps others too Matcher matcher = GROUP_PATTERN.matcher(item.getDescription()); if (matcher.matches() && !Objects.equals(matcher.group(1), "not available")) { @@ -763,7 +782,7 @@ public class Newznab extends Indexer { @Component @Order(500) - public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { + public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { @Override public boolean handlesIndexerConfig(IndexerConfig config) { @@ -771,9 +790,11 @@ public class Newznab extends Indexer { } @Override - public Class getIndexerClass() { - return Newznab.class; + public String getName() { + return "NEWZNAB"; } + + } diff --git a/core/src/main/java/org/nzbhydra/indexers/NfoResult.java b/core/src/main/java/org/nzbhydra/indexers/NfoResult.java index 49b322d6f..44dda56f1 100644 --- a/core/src/main/java/org/nzbhydra/indexers/NfoResult.java +++ b/core/src/main/java/org/nzbhydra/indexers/NfoResult.java @@ -1,10 +1,12 @@ package org.nzbhydra.indexers; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.io.Serializable; @Data +@ReflectionMarker public class NfoResult implements Serializable { private boolean successful; diff --git a/core/src/main/java/org/nzbhydra/indexers/NzbGeek.java b/core/src/main/java/org/nzbhydra/indexers/NzbGeek.java index f49c3adf1..2e60efa4f 100644 --- a/core/src/main/java/org/nzbhydra/indexers/NzbGeek.java +++ b/core/src/main/java/org/nzbhydra/indexers/NzbGeek.java @@ -1,17 +1,31 @@ package org.nzbhydra.indexers; import com.google.common.base.Joiner; +import org.nzbhydra.config.BaseConfigHandler; +import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; +import org.nzbhydra.indexers.status.IndexerLimitRepository; +import org.nzbhydra.mediainfo.InfoProvider; +import org.nzbhydra.searching.CategoryProvider; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; +import org.nzbhydra.searching.SearchResultAcceptor; +import org.nzbhydra.searching.db.SearchResultRepository; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.annotation.Order; +import org.springframework.oxm.Unmarshaller; import org.springframework.stereotype.Component; import java.util.Arrays; -@Component public class NzbGeek extends Newznab { + + public NzbGeek(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, Unmarshaller unmarshaller, BaseConfigHandler baseConfigHandler) { + super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler); + } + @Override protected String cleanupQuery(String query) { //With nzbgeek not more than 6 words at all are allowed @@ -24,7 +38,7 @@ public class NzbGeek extends Newznab { @Component @Order(100) - public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { + public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { @Override public boolean handlesIndexerConfig(IndexerConfig config) { @@ -32,8 +46,8 @@ public class NzbGeek extends Newznab { } @Override - public Class getIndexerClass() { - return NzbGeek.class; + public String getName() { + return "NZBGEEK"; } } diff --git a/core/src/main/java/org/nzbhydra/indexers/NzbIndex.java b/core/src/main/java/org/nzbhydra/indexers/NzbIndex.java index 86743aaaa..af4b4a0be 100644 --- a/core/src/main/java/org/nzbhydra/indexers/NzbIndex.java +++ b/core/src/main/java/org/nzbhydra/indexers/NzbIndex.java @@ -2,21 +2,30 @@ package org.nzbhydra.indexers; import com.google.common.base.Joiner; import joptsimple.internal.Strings; +import org.nzbhydra.config.BaseConfigHandler; +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; +import org.nzbhydra.indexers.status.IndexerLimitRepository; import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; +import org.nzbhydra.mediainfo.InfoProvider; +import org.nzbhydra.searching.CategoryProvider; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; +import org.nzbhydra.searching.SearchResultAcceptor; import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult; +import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.HasNfo; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; @@ -28,13 +37,16 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -@Component public class NzbIndex extends Indexer { private static final Logger logger = LoggerFactory.getLogger(NzbIndex.class); private static final Pattern GUID_PATTERN = Pattern.compile(".*/download/(\\d+).*", Pattern.DOTALL); private static final Pattern NFO_PATTERN = Pattern.compile(".*
(.*)
.*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + public NzbIndex(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, BaseConfigHandler baseConfigHandler) { + super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler); + } + @Override protected void completeIndexerSearchResult(NewznabXmlRoot response, IndexerSearchResult indexerSearchResult, AcceptorResult acceptorResult, SearchRequest searchRequest, int offset, Integer limit) { //Never provide more than the first 250 results, RSS doesn't allow paging @@ -176,7 +188,7 @@ public class NzbIndex extends Indexer { @Component @Order(2000) - public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { + public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { @Override public boolean handlesIndexerConfig(IndexerConfig config) { @@ -184,8 +196,8 @@ public class NzbIndex extends Indexer { } @Override - public Class getIndexerClass() { - return NzbIndex.class; + public String getName() { + return "NZBINDEX"; } } diff --git a/core/src/main/java/org/nzbhydra/indexers/QueryGenerator.java b/core/src/main/java/org/nzbhydra/indexers/QueryGenerator.java index 52a922494..655b3ac8b 100644 --- a/core/src/main/java/org/nzbhydra/indexers/QueryGenerator.java +++ b/core/src/main/java/org/nzbhydra/indexers/QueryGenerator.java @@ -20,13 +20,13 @@ import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; import org.nzbhydra.mapping.newznab.ActionAttribute; import org.nzbhydra.mediainfo.InfoProvider; import org.nzbhydra.mediainfo.InfoProviderException; -import org.nzbhydra.mediainfo.MediaIdType; import org.nzbhydra.mediainfo.MediaInfo; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.InternalData; import org.nzbhydra.searching.searchrequests.SearchRequest; import org.slf4j.Logger; @@ -41,12 +41,6 @@ import java.util.concurrent.TimeUnit; @Component public class QueryGenerator { - public enum QueryFormat { - TITLE, - TITLE_YEAR, - TITLE_YEAR_LANGUAGE - } - private static final Logger logger = LoggerFactory.getLogger(QueryGenerator.class); private final Map generatedQueries = @@ -70,7 +64,7 @@ public class QueryGenerator { boolean anyIdsAvailable = !searchRequest.getIdentifiers().isEmpty(); boolean indexerDoesntSupportAnyOfTheProvidedIds = anyIdsAvailable && searchRequest.getIdentifiers().keySet().stream().noneMatch(x -> config.getSupportedSearchIds().contains(x)); boolean queryGenerationPossible = !searchRequest.getIdentifiers().isEmpty() || searchRequest.getTitle().isPresent(); - boolean queryGenerationEnabled = configProvider.getBaseConfig().getSearching().getGenerateQueries().meets(searchRequest); + boolean queryGenerationEnabled = searchRequest.meets(configProvider.getBaseConfig().getSearching().getGenerateQueries()); final InternalData.FallbackState fallbackState = searchRequest.getInternalData().getFallbackStateByIndexer(config.getName()); boolean fallbackRequested = fallbackState == InternalData.FallbackState.REQUESTED; @@ -93,12 +87,12 @@ public class QueryGenerator { logger.debug("Using internally provided title {}", query); } else { Optional> firstIdentifierEntry = searchRequest.getIdentifiers().entrySet().stream().filter(java.util.Objects::nonNull).findFirst(); - if (!firstIdentifierEntry.isPresent()) { + if (firstIdentifierEntry.isEmpty()) { throw new IndexerSearchAbortedException("Unable to generate query because no identifier is known"); } try { MediaInfo mediaInfo = infoProvider.convert(firstIdentifierEntry.get().getValue(), firstIdentifierEntry.get().getKey()); - if (!mediaInfo.getTitle().isPresent()) { + if (mediaInfo.getTitle().isEmpty()) { throw new IndexerSearchAbortedException("Unable to generate query because no title is known"); } query = sanitizeTitleForQuery(mediaInfo.getTitle().get()); diff --git a/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerChecker.java b/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerChecker.java index 678b756b1..6cc26c5f3 100644 --- a/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerChecker.java +++ b/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerChecker.java @@ -23,14 +23,17 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.GenericResponse; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.indexer.BackendType; +import org.nzbhydra.config.indexer.CapsCheckRequest; +import org.nzbhydra.config.indexer.CapsCheckRequest.CheckType; +import org.nzbhydra.config.indexer.CheckCapsResponse; import org.nzbhydra.config.indexer.IndexerCategoryConfig; import org.nzbhydra.config.indexer.IndexerCategoryConfig.MainCategory; import org.nzbhydra.config.indexer.IndexerCategoryConfig.SubCategory; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; -import org.nzbhydra.indexers.Indexer.BackendType; +import org.nzbhydra.config.mediainfo.MediaIdType; import org.nzbhydra.indexers.IndexerWebAccess; -import org.nzbhydra.indexers.capscheck.CapsCheckRequest.CheckType; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.logging.MdcThreadPoolExecutor; @@ -41,8 +44,8 @@ import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; import org.nzbhydra.mapping.newznab.xml.Xml; import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlCategory; import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlRoot; -import org.nzbhydra.mediainfo.MediaIdType; import org.nzbhydra.searching.SearchModuleProvider; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -219,10 +222,10 @@ public class IndexerChecker { Optional responseWithLimits = responses.stream().filter(x -> x.getApiMax() != null).findFirst(); if (responseWithLimits.isPresent()) { logger.info("Determined an api hit limit of {} and a download limit of {}", responseWithLimits.get().apiMax, responseWithLimits.get().downloadsMax); - if (!indexerConfig.getHitLimit().isPresent() && responseWithLimits.get().apiMax > -1) { + if (indexerConfig.getHitLimit().isEmpty() && responseWithLimits.get().apiMax > -1) { indexerConfig.setHitLimit(responseWithLimits.get().apiMax); } - if (!indexerConfig.getDownloadLimit().isPresent() && responseWithLimits.get().downloadsMax > -1) { + if (indexerConfig.getDownloadLimit().isEmpty() && responseWithLimits.get().downloadsMax > -1) { indexerConfig.setDownloadLimit(responseWithLimits.get().downloadsMax); } } @@ -280,7 +283,7 @@ public class IndexerChecker { && (checkType == CheckType.ALL || !x.isAllCapsChecked()); List configsToCheck = configProvider.getBaseConfig().getIndexers().stream() .filter(isToBeCheckedPredicate) - .collect(Collectors.toList()); + .toList(); if (configsToCheck.isEmpty()) { logger.info("No indexers to check"); return Collections.emptyList(); @@ -442,6 +445,7 @@ public class IndexerChecker { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class CheckerEvent { @@ -450,6 +454,7 @@ public class IndexerChecker { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class ConnectionCheckResponse { @@ -459,6 +464,7 @@ public class IndexerChecker { @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor private static class CheckCapsRequest { @@ -471,6 +477,7 @@ public class IndexerChecker { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor private static class SingleCheckCapsResponse { diff --git a/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerWeb.java b/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerWeb.java index 37af0ecb1..6d946b164 100644 --- a/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerWeb.java +++ b/core/src/main/java/org/nzbhydra/indexers/capscheck/IndexerWeb.java @@ -21,9 +21,12 @@ import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import lombok.Data; import org.nzbhydra.GenericResponse; +import org.nzbhydra.config.indexer.CapsCheckRequest; +import org.nzbhydra.config.indexer.CheckCapsResponse; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; import org.nzbhydra.indexers.capscheck.IndexerChecker.CheckerEvent; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -143,12 +146,14 @@ public class IndexerWeb { } @Data +@ReflectionMarker private static class JacketConfigReadRequest { private List existingIndexers; private IndexerConfig jackettConfig; } @Data +@ReflectionMarker private static class JacketConfigReadResponse { private List newIndexersConfig; private int addedTrackers; diff --git a/core/src/main/java/org/nzbhydra/indexers/capscheck/JacketConfigRetriever.java b/core/src/main/java/org/nzbhydra/indexers/capscheck/JacketConfigRetriever.java index fe4a4ac8a..ac2811c94 100644 --- a/core/src/main/java/org/nzbhydra/indexers/capscheck/JacketConfigRetriever.java +++ b/core/src/main/java/org/nzbhydra/indexers/capscheck/JacketConfigRetriever.java @@ -19,6 +19,7 @@ package org.nzbhydra.indexers.capscheck; import org.nzbhydra.Jackson; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; +import org.nzbhydra.config.mediainfo.MediaIdType; import org.nzbhydra.indexers.IndexerWebAccess; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.mapping.newznab.ActionAttribute; @@ -26,7 +27,6 @@ import org.nzbhydra.mapping.newznab.xml.NewznabXmlError; import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlSearching; import org.nzbhydra.mapping.newznab.xml.caps.jackett.JacketCapsXmlIndexer; import org.nzbhydra.mapping.newznab.xml.caps.jackett.JacketCapsXmlRoot; -import org.nzbhydra.mediainfo.MediaIdType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -59,14 +59,12 @@ public class JacketConfigRetriever { .build().toUri(); logger.info("Getting configured jackett trackers from {}", uri); final Object response = indexerWebAccess.get(uri, jackettConfig); - if (response instanceof NewznabXmlError) { - NewznabXmlError error = (NewznabXmlError) response; + if (response instanceof NewznabXmlError error) { throw new IndexerAccessException("Jackett report error " + error.getCode() + ": " + error.getDescription()); } - if (!(response instanceof JacketCapsXmlRoot)) { + if (!(response instanceof JacketCapsXmlRoot root)) { throw new IOException("Unable to parse response from jackett"); } - JacketCapsXmlRoot root = (JacketCapsXmlRoot) response; List configs = new ArrayList<>(); for (JacketCapsXmlIndexer indexer : root.getIndexers()) { diff --git a/core/src/main/java/org/nzbhydra/indexers/status/IndexerLimit.java b/core/src/main/java/org/nzbhydra/indexers/status/IndexerLimit.java index da2ab2bcc..89119cfdb 100644 --- a/core/src/main/java/org/nzbhydra/indexers/status/IndexerLimit.java +++ b/core/src/main/java/org/nzbhydra/indexers/status/IndexerLimit.java @@ -1,24 +1,28 @@ package org.nzbhydra.indexers.status; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; import org.nzbhydra.indexers.IndexerEntity; +import org.nzbhydra.springnative.ReflectionMarker; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.OneToOne; -import javax.persistence.Table; import java.time.Instant; @Data +@ReflectionMarker @Entity @Table(name = "indexerlimit") -public class IndexerLimit { +public final class IndexerLimit { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @SequenceGenerator(allocationSize = 1, name = "INDEXERLIMIT_SEQ") protected int id; @OneToOne diff --git a/core/src/main/java/org/nzbhydra/indexers/status/IndexerStatusesAndLimits.java b/core/src/main/java/org/nzbhydra/indexers/status/IndexerStatusesAndLimits.java index f063603af..a627936be 100644 --- a/core/src/main/java/org/nzbhydra/indexers/status/IndexerStatusesAndLimits.java +++ b/core/src/main/java/org/nzbhydra/indexers/status/IndexerStatusesAndLimits.java @@ -16,6 +16,8 @@ package org.nzbhydra.indexers.status; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -24,13 +26,12 @@ import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.indexers.IndexerEntity; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.searching.SearchModuleProvider; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.persistence.EntityManager; -import javax.persistence.Query; import java.sql.Timestamp; import java.time.Clock; import java.time.Instant; @@ -147,7 +148,7 @@ public class IndexerStatusesAndLimits { private Instant getResetTime(IndexerConfig indexerConfig) { - if (!indexerConfig.getHitLimitResetTime().isPresent()) { + if (indexerConfig.getHitLimitResetTime().isEmpty()) { return null; } LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); @@ -159,6 +160,7 @@ public class IndexerStatusesAndLimits { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class IndexerStatus { diff --git a/core/src/main/java/org/nzbhydra/indexers/torznab/Torznab.java b/core/src/main/java/org/nzbhydra/indexers/torznab/Torznab.java index 45b328a26..c9d68d4b4 100644 --- a/core/src/main/java/org/nzbhydra/indexers/torznab/Torznab.java +++ b/core/src/main/java/org/nzbhydra/indexers/torznab/Torznab.java @@ -19,27 +19,41 @@ package org.nzbhydra.indexers.torznab; import lombok.Getter; import lombok.Setter; import org.nzbhydra.NzbHydraException; +import org.nzbhydra.config.BaseConfigHandler; +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; -import org.nzbhydra.indexers.Indexer; +import org.nzbhydra.indexers.IndexerApiAccessEntityShortRepository; +import org.nzbhydra.indexers.IndexerApiAccessRepository; import org.nzbhydra.indexers.IndexerHandlingStrategy; +import org.nzbhydra.indexers.IndexerRepository; +import org.nzbhydra.indexers.IndexerWebAccess; import org.nzbhydra.indexers.Newznab; +import org.nzbhydra.indexers.QueryGenerator; import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; +import org.nzbhydra.indexers.status.IndexerLimitRepository; import org.nzbhydra.mapping.newznab.xml.NewznabAttribute; import org.nzbhydra.mapping.newznab.xml.NewznabXmlChannel; import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; import org.nzbhydra.mapping.newznab.xml.Xml; +import org.nzbhydra.mediainfo.InfoProvider; +import org.nzbhydra.searching.CategoryProvider; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; +import org.nzbhydra.searching.SearchResultAcceptor; import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult; import org.nzbhydra.searching.SearchResultIdCalculator; +import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.HasNfo; import org.nzbhydra.searching.searchrequests.SearchRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.annotation.Order; +import org.springframework.oxm.Unmarshaller; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; @@ -47,15 +61,17 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; @Getter @Setter -@Component public class Torznab extends Newznab { private static final Logger logger = LoggerFactory.getLogger(Torznab.class); + public Torznab(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, Unmarshaller unmarshaller, BaseConfigHandler baseConfigHandler) { + super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler); + } + protected SearchResultItem createSearchResultItem(NewznabXmlItem item) throws NzbHydraException { item.getRssGuid().setPermaLink(true); //Not set in RSS but actually always true SearchResultItem searchResultItem = super.createSearchResultItem(item); @@ -64,18 +80,10 @@ public class Torznab extends Newznab { for (NewznabAttribute attribute : item.getTorznabAttributes()) { searchResultItem.getAttributes().put(attribute.getName(), attribute.getValue()); switch (attribute.getName()) { - case "grabs": - searchResultItem.setGrabs(Integer.valueOf(attribute.getValue())); - break; - case "guid": - searchResultItem.setIndexerGuid(attribute.getValue()); - break; - case "seeders": - searchResultItem.setSeeders(Integer.valueOf(attribute.getValue())); - break; - case "peers": - searchResultItem.setPeers(Integer.valueOf(attribute.getValue())); - break; + case "grabs" -> searchResultItem.setGrabs(Integer.valueOf(attribute.getValue())); + case "guid" -> searchResultItem.setIndexerGuid(attribute.getValue()); + case "seeders" -> searchResultItem.setSeeders(Integer.valueOf(attribute.getValue())); + case "peers" -> searchResultItem.setPeers(Integer.valueOf(attribute.getValue())); } } if (item.getSize() != null) { @@ -108,8 +116,8 @@ public class Torznab extends Newznab { } } - foundCategories.addAll(item.getNewznabAttributes().stream().filter(x -> x.getName().equals("category")).map(x -> Integer.valueOf(x.getValue())).collect(Collectors.toList())); - foundCategories.addAll(item.getTorznabAttributes().stream().filter(x -> x.getName().equals("category")).map(x -> Integer.valueOf(x.getValue())).collect(Collectors.toList())); + foundCategories.addAll(item.getNewznabAttributes().stream().filter(x -> x.getName().equals("category")).map(x -> Integer.valueOf(x.getValue())).toList()); + foundCategories.addAll(item.getTorznabAttributes().stream().filter(x -> x.getName().equals("category")).map(x -> Integer.valueOf(x.getValue())).toList()); return new ArrayList<>(foundCategories); } @@ -155,7 +163,7 @@ public class Torznab extends Newznab { @Component @Order(2000) - public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { + public static class NewznabHandlingStrategy implements IndexerHandlingStrategy { @Override public boolean handlesIndexerConfig(IndexerConfig config) { @@ -163,9 +171,10 @@ public class Torznab extends Newznab { } @Override - public Class getIndexerClass() { - return Torznab.class; + public String getName() { + return "TORZNAB"; } + } diff --git a/core/src/main/java/org/nzbhydra/logging/ColorConverter.java b/core/src/main/java/org/nzbhydra/logging/ColorConverter.java index 6596230b4..137a4c200 100644 --- a/core/src/main/java/org/nzbhydra/logging/ColorConverter.java +++ b/core/src/main/java/org/nzbhydra/logging/ColorConverter.java @@ -30,7 +30,7 @@ public class ColorConverter extends org.springframework.boot.logging.logback.Col private static final Map LEVELS; static { - Map levels = new HashMap(); + Map levels = new HashMap<>(); levels.put(Level.DEBUG_INTEGER, AnsiColor.BLUE); //Only change in implementation, use different color for debug levels.put(Level.ERROR_INTEGER, AnsiColor.RED); levels.put(Level.WARN_INTEGER, AnsiColor.YELLOW); diff --git a/core/src/main/java/org/nzbhydra/logging/EceptionFilter.java b/core/src/main/java/org/nzbhydra/logging/EceptionFilter.java index 54aa7fb1a..0fddde0c6 100644 --- a/core/src/main/java/org/nzbhydra/logging/EceptionFilter.java +++ b/core/src/main/java/org/nzbhydra/logging/EceptionFilter.java @@ -22,10 +22,10 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.spi.FilterReply; +import jakarta.annotation.PostConstruct; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import java.util.Iterator; /** diff --git a/core/src/main/java/org/nzbhydra/logging/LogAnonymizer.java b/core/src/main/java/org/nzbhydra/logging/LogAnonymizer.java index 5508b32c0..726426efb 100644 --- a/core/src/main/java/org/nzbhydra/logging/LogAnonymizer.java +++ b/core/src/main/java/org/nzbhydra/logging/LogAnonymizer.java @@ -81,16 +81,16 @@ public class LogAnonymizer { private String removeIpsFromLog(String log) { logger.debug("Removing IPs and hostnames from log"); log = log.replaceAll("Host: .*\\..*\\]", "Host: ]"); - log = log.replaceAll("127\\.0\\.0\\.1", ""); + log = log.replace("127.0.0.1", ""); log = replaceWithHashedValues(log, IPV4_PATTERN, "IP4"); - log = replaceWithHashedValues(log, IPV6_PATTERN, "IP6"); +// log = replaceWithHashedValues(log, IPV6_PATTERN, "IP6"); return log; } private String replaceWithHashedValues(String log, String regex, final String tag) { Pattern ipPattern = Pattern.compile(regex); Matcher matcher = ipPattern.matcher(log); - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); while (matcher.find()) { if (matcher.group(0).equals("127.0.0.1") || matcher.group(0).startsWith("192.168") || matcher.group(0).startsWith("10.")) { continue; diff --git a/core/src/main/java/org/nzbhydra/logging/LogContentProvider.java b/core/src/main/java/org/nzbhydra/logging/LogContentProvider.java index 92a609588..b8cbba912 100644 --- a/core/src/main/java/org/nzbhydra/logging/LogContentProvider.java +++ b/core/src/main/java/org/nzbhydra/logging/LogContentProvider.java @@ -11,6 +11,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.Jackson; import org.nzbhydra.NzbHydra; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -62,7 +63,7 @@ public class LogContentProvider { if (logFiles == null) { return Collections.emptyList(); } - return Stream.of(logFiles).sorted(Comparator.comparingLong(File::lastModified).reversed()).filter(x -> x.getName().toLowerCase().endsWith("log")).map(File::getName).collect(Collectors.toList()); + return Stream.of(logFiles).sorted(Comparator.comparingLong(File::lastModified).reversed()).map(File::getName).filter(name -> name.toLowerCase().endsWith("log")).collect(Collectors.toList()); } public JsonLogResponse getLogsAsJsonLines(int offset, int limit) throws IOException { @@ -75,23 +76,25 @@ public class LogContentProvider { } List> objects = new ArrayList<>(); int count = 0; - ReversedLinesFileReader reversedLinesFileReader = new ReversedLinesFileReader(logfile, Charset.defaultCharset()); - String line = reversedLinesFileReader.readLine(); - while (offset > 0 && count++ < offset && line != null) { + String line; + try (ReversedLinesFileReader reversedLinesFileReader = new ReversedLinesFileReader(logfile, Charset.defaultCharset())) { line = reversedLinesFileReader.readLine(); - } - if (count > 0 && line == null) { - return new JsonLogResponse(Collections.emptyList(), false, offset, 0); - } - count = 1; - while (line != null && count++ <= limit) { - TypeReference> typeRef - = new TypeReference>() { - }; + while (offset > 0 && count++ < offset && line != null) { + line = reversedLinesFileReader.readLine(); + } + if (count > 0 && line == null) { + return new JsonLogResponse(Collections.emptyList(), false, offset, 0); + } + count = 1; + while (line != null && count++ <= limit) { + TypeReference> typeRef + = new TypeReference<>() { + }; - HashMap o = Jackson.JSON_MAPPER.readValue(line, typeRef); - objects.add(o); - line = reversedLinesFileReader.readLine(); + HashMap o = Jackson.JSON_MAPPER.readValue(line, typeRef); + objects.add(o); + line = reversedLinesFileReader.readLine(); + } } return new JsonLogResponse(objects, line != null, offset, objects.size()); @@ -108,8 +111,7 @@ public class LogContentProvider { for (Iterator> index = logger.iteratorForAppenders(); index.hasNext(); ) { Object enumElement = index.next(); - if (enumElement instanceof FileAppender) { - FileAppender temp = (FileAppender) enumElement; + if (enumElement instanceof FileAppender temp) { if (getJsonFile) { if (!temp.getEncoder().getClass().getName().equals(SensitiveDataRemovingPatternLayoutEncoder.class.getName())) { fileAppender = temp; @@ -135,6 +137,7 @@ public class LogContentProvider { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class JsonLogResponse { diff --git a/core/src/main/java/org/nzbhydra/logging/LoggingMarkerFilter.java b/core/src/main/java/org/nzbhydra/logging/LoggingMarkerFilter.java index 747c8b5a7..e472a7964 100644 --- a/core/src/main/java/org/nzbhydra/logging/LoggingMarkerFilter.java +++ b/core/src/main/java/org/nzbhydra/logging/LoggingMarkerFilter.java @@ -7,6 +7,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.spi.FilterReply; +import jakarta.annotation.PostConstruct; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigChangedEvent; import org.nzbhydra.config.ConfigProvider; @@ -16,7 +17,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import java.util.HashSet; import java.util.Iterator; import java.util.Set; diff --git a/core/src/main/java/org/nzbhydra/logging/MdcThreadPoolExecutor.java b/core/src/main/java/org/nzbhydra/logging/MdcThreadPoolExecutor.java index 1da498572..b54d33a45 100644 --- a/core/src/main/java/org/nzbhydra/logging/MdcThreadPoolExecutor.java +++ b/core/src/main/java/org/nzbhydra/logging/MdcThreadPoolExecutor.java @@ -39,13 +39,12 @@ public class MdcThreadPoolExecutor extends ThreadPoolExecutor { */ public static MdcThreadPoolExecutor newWithInheritedMdc(int corePoolSize) { return new MdcThreadPoolExecutor(null, corePoolSize, corePoolSize, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue()); + new LinkedBlockingQueue<>()); } /** * Pool where task threads take fixed MDC from the thread that creates the pool. */ - @SuppressWarnings("unchecked") public static MdcThreadPoolExecutor newWithCurrentMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) { return new MdcThreadPoolExecutor(MDC.getCopyOfContextMap(), corePoolSize, maximumPoolSize, keepAliveTime, unit, @@ -69,7 +68,6 @@ public class MdcThreadPoolExecutor extends ThreadPoolExecutor { this.instantiationTime = Instant.now(); } - @SuppressWarnings("unchecked") private Map getContextForTask() { return useFixedContext ? fixedContext : MDC.getCopyOfContextMap(); } @@ -88,10 +86,9 @@ public class MdcThreadPoolExecutor extends ThreadPoolExecutor { if (this == o) { return true; } - if (!(o instanceof MdcThreadPoolExecutor)) { + if (!(o instanceof MdcThreadPoolExecutor that)) { return false; } - MdcThreadPoolExecutor that = (MdcThreadPoolExecutor) o; return Objects.equal(instantiationTime, that.instantiationTime); } @@ -122,4 +119,4 @@ public class MdcThreadPoolExecutor extends ThreadPoolExecutor { } }; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/nzbhydra/logging/ProgressLogger.java b/core/src/main/java/org/nzbhydra/logging/ProgressLogger.java index 368b5d5ae..6314443d5 100644 --- a/core/src/main/java/org/nzbhydra/logging/ProgressLogger.java +++ b/core/src/main/java/org/nzbhydra/logging/ProgressLogger.java @@ -345,7 +345,7 @@ public final class ProgressLogger { return logger; } - private final String itemName() { + private String itemName() { if (Objects.equals(referenceItemsName, itemsName)) { return itemName; } @@ -492,7 +492,6 @@ public final class ProgressLogger { lastLogTime = currentTime; lastCount = count; - return; } /** diff --git a/core/src/main/java/org/nzbhydra/logging/ReversedLinesFileReader.java b/core/src/main/java/org/nzbhydra/logging/ReversedLinesFileReader.java index a3b7a95c9..943c5b9d2 100644 --- a/core/src/main/java/org/nzbhydra/logging/ReversedLinesFileReader.java +++ b/core/src/main/java/org/nzbhydra/logging/ReversedLinesFileReader.java @@ -18,7 +18,11 @@ package org.nzbhydra.logging; import org.apache.commons.io.Charsets; -import java.io.*; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; @@ -35,9 +39,6 @@ public class ReversedLinesFileReader implements Closeable { private final RandomAccessFile randomAccessFile; - private final long totalByteLength; - private final long totalBlockCount; - private final byte[][] newLineSequences; private final int avoidNewlineSplitBufferSize; private final int byteDecrement; @@ -124,8 +125,9 @@ public class ReversedLinesFileReader implements Closeable { // Open file randomAccessFile = new RandomAccessFile(file, "r"); - totalByteLength = randomAccessFile.length(); + long totalByteLength = randomAccessFile.length(); int lastBlockLength = (int) (totalByteLength % blockSize); + long totalBlockCount; if (lastBlockLength > 0) { totalBlockCount = totalByteLength / blockSize + 1; } else { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/DownloadType.java b/core/src/main/java/org/nzbhydra/mediainfo/AutocompleteType.java similarity index 79% rename from core/src/main/java/org/nzbhydra/searching/dtoseventsenums/DownloadType.java rename to core/src/main/java/org/nzbhydra/mediainfo/AutocompleteType.java index 754a12b5f..eac854eab 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/DownloadType.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/AutocompleteType.java @@ -1,5 +1,5 @@ /* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,9 @@ * limitations under the License. */ -package org.nzbhydra.searching.dtoseventsenums; +package org.nzbhydra.mediainfo; -public enum DownloadType { - - NZB, - TORRENT +enum AutocompleteType { + TV, + MOVIE } diff --git a/core/src/main/java/org/nzbhydra/mediainfo/CustomTmdb.java b/core/src/main/java/org/nzbhydra/mediainfo/CustomTmdb.java deleted file mode 100644 index b472cc4cb..000000000 --- a/core/src/main/java/org/nzbhydra/mediainfo/CustomTmdb.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.nzbhydra.mediainfo; - -import com.uwetrottmann.tmdb2.Tmdb; -import com.uwetrottmann.tmdb2.TmdbHelper; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClient.Builder; -import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -import javax.annotation.PostConstruct; -import java.net.URI; -import java.net.URISyntaxException; - -@Component -public class CustomTmdb extends Tmdb { - - public static final String API_HOST = "api.themoviedb.org"; - public static final String API_VERSION = "3"; - public static String API_URL = "https://" + API_HOST + "/" + API_VERSION + "/"; - - @Value("${nzbhydra.tmdb.apikey:}") - protected String tmdbApiKey; - protected OkHttpClient client; - - @Autowired - protected HydraOkHttp3ClientHttpRequestFactory requestFactory; - - public CustomTmdb() { - super(null); - } - - public CustomTmdb(String apiKey) { - super(apiKey); - } - - @PostConstruct - private void initWithApiKey() { - this.apiKey(tmdbApiKey); - } - - @Override - protected Retrofit.Builder retrofitBuilder() { - return new Retrofit.Builder() - .baseUrl(API_URL) - .addConverterFactory(GsonConverterFactory.create(TmdbHelper.getGsonBuilder().create())) - .client(okHttpClient()); - } - - - @Override - protected synchronized OkHttpClient okHttpClient() { - if (client == null) { - try { - Builder builder = requestFactory.getOkHttpClientBuilder(new URI(Tmdb.API_URL)); - setOkHttpClientDefaults(builder); - client = builder.build(); - } catch (URISyntaxException e) { - throw new RuntimeException("Shouldn't happen...", e); - } - } - return client; - } - - public static void setApiUrl(String apiUrl) { - API_URL = apiUrl; - } - - public static void setApiUrlFromHttpHost(String host) { - API_URL = "http://" + host + "/" + API_VERSION + "/"; - } - - -} diff --git a/core/src/main/java/org/nzbhydra/mediainfo/InfoProvider.java b/core/src/main/java/org/nzbhydra/mediainfo/InfoProvider.java index f0ad8d66b..2a4a07974 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/InfoProvider.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/InfoProvider.java @@ -4,6 +4,7 @@ import com.google.common.base.Throwables; import com.google.common.collect.Sets; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; +import org.nzbhydra.config.mediainfo.MediaIdType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -19,7 +20,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.nzbhydra.mediainfo.MediaIdType.*; +import static org.nzbhydra.config.mediainfo.MediaIdType.*; @Component public class InfoProvider { @@ -181,7 +182,7 @@ public class InfoProvider { List infos; //Always do a search and don't rely on the database, otherwise results might be outdated switch (titleType) { - case TVTITLE: { + case TVTITLE -> { List results = tvMazeHandler.search(title); infos = results.stream().map(MediaInfo::new).collect(Collectors.toList()); for (MediaInfo mediaInfo : infos) { @@ -190,16 +191,13 @@ public class InfoProvider { tvInfoRepository.save(tvInfo); } } - break; } - case MOVIETITLE: { + case MOVIETITLE -> { List results = tmdbHandler.search(title, null); infos = results.stream().map(MediaInfo::new).collect(Collectors.toList()); //Do not save these infos to database because TMDB only returns basic info. IMDB ID might be missing and when the repository is queried for conversion it returns an empty IMDB ID - break; } - default: - throw new IllegalArgumentException("Wrong IdType"); + default -> throw new IllegalArgumentException("Wrong IdType"); } diff --git a/core/src/main/java/org/nzbhydra/mediainfo/MediaInfo.java b/core/src/main/java/org/nzbhydra/mediainfo/MediaInfo.java index fe86ccbe8..6998fdb36 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/MediaInfo.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/MediaInfo.java @@ -3,6 +3,7 @@ package org.nzbhydra.mediainfo; import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.MoreObjects; import lombok.Setter; +import org.nzbhydra.config.mediainfo.MediaIdType; import java.util.Collection; import java.util.Optional; @@ -53,23 +54,15 @@ public class MediaInfo { @JsonIgnore public Optional getByIdType(MediaIdType idType) { - switch (idType) { - case IMDB: - case TVIMDB: - return getImdbId(); - case TMDB: - return getTmdbId(); - case TVMAZE: - return getTvMazeId(); - case TVRAGE: - return getTvRageId(); - case TVDB: - return getTvDbId(); - case TVTITLE: - case MOVIETITLE: - return getTitle(); - } - return Optional.empty(); + return switch (idType) { + case IMDB, TVIMDB -> getImdbId(); + case TMDB -> getTmdbId(); + case TVMAZE -> getTvMazeId(); + case TVRAGE -> getTvRageId(); + case TVDB -> getTvDbId(); + case TVTITLE, MOVIETITLE -> getTitle(); + default -> Optional.empty(); + }; } @JsonIgnore diff --git a/core/src/main/java/org/nzbhydra/mediainfo/MediaInfoWeb.java b/core/src/main/java/org/nzbhydra/mediainfo/MediaInfoWeb.java index 89bd53e5f..63187ab14 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/MediaInfoWeb.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/MediaInfoWeb.java @@ -5,10 +5,13 @@ import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.net.UrlEscapers; +import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -19,7 +22,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; import java.util.List; @@ -38,29 +40,29 @@ public class MediaInfoWeb { @Autowired private ConfigProvider configProvider; - private LoadingCache> autocompleteCache = CacheBuilder.newBuilder() - .maximumSize(100) - .expireAfterWrite(7, TimeUnit.DAYS) + private final LoadingCache> autocompleteCache = CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterWrite(7, TimeUnit.DAYS) - .build( - new CacheLoader>() { - @Override - public List load(CacheKey key) throws Exception { - try { - List infos; - if (key.getType() == AutocompleteType.TV) { - infos = infoProvider.search(key.getInput(), MediaIdType.TVTITLE); - } else { - infos = infoProvider.search(key.getInput(), MediaIdType.MOVIETITLE); - } - - return infos.stream().map(MediaInfoTO::new).collect(Collectors.toList()); - } catch (InfoProviderException e) { - logger.error("Error while finding autocomplete data for input {} and type {}", key.getInput(), key.getType(), e); - return Collections.emptyList(); + .build( + new CacheLoader<>() { + @Override + public List load(CacheKey key) throws Exception { + try { + List infos; + if (key.getType() == AutocompleteType.TV) { + infos = infoProvider.search(key.getInput(), MediaIdType.TVTITLE); + } else { + infos = infoProvider.search(key.getInput(), MediaIdType.MOVIETITLE); } + + return infos.stream().map(MediaInfoWeb::from).collect(Collectors.toList()); + } catch (InfoProviderException e) { + logger.warn("Error while finding autocomplete data for input {} and type {}", key.getInput(), key.getType(), e); + return Collections.emptyList(); } - }); + } + }); @RequestMapping(value = "/internalapi/autocomplete/{type}", produces = "application/json") public List autocomplete(@PathVariable("type") AutocompleteType type, @RequestParam("input") String input) throws ExecutionException { @@ -102,7 +104,21 @@ public class MediaInfoWeb { } + private static MediaInfoTO from(MediaInfo info) { + MediaInfoTO to = new MediaInfoTO(); + to.setImdbId(info.getImdbId().orElse(null)); + to.setTmdbId(info.getTmdbId().orElse(null)); + to.setTvmazeId(info.getTvMazeId().orElse(null)); + to.setTvrageId(info.getTvRageId().orElse(null)); + to.setTvdbId(info.getTvDbId().orElse(null)); + to.setTitle(info.getTitle().orElse(null)); + to.setYear(info.getYear().orElse(null)); + to.setPosterUrl(info.getPosterUrl().orElse(null)); + return to; + } + @Data + @ReflectionMarker @AllArgsConstructor @NoArgsConstructor private static class CacheKey { @@ -110,32 +126,4 @@ public class MediaInfoWeb { private String input; } - private enum AutocompleteType { - TV, - MOVIE - } - - @Data - public static class MediaInfoTO { - private String imdbId; - private String tmdbId; - private String tvmazeId; - private String tvrageId; - private String tvdbId; - private String title; - private Integer year; - private String posterUrl; - - public MediaInfoTO(MediaInfo info) { - this.imdbId = info.getImdbId().orElse(null); - this.tmdbId = info.getTmdbId().orElse(null); - this.tvmazeId = info.getTvMazeId().orElse(null); - this.tvrageId = info.getTvRageId().orElse(null); - this.tvdbId = info.getTvDbId().orElse(null); - this.title = info.getTitle().orElse(null); - this.year = info.getYear().orElse(null); - this.posterUrl = info.getPosterUrl().orElse(null); - } - - } } diff --git a/core/src/main/java/org/nzbhydra/mediainfo/MovieInfo.java b/core/src/main/java/org/nzbhydra/mediainfo/MovieInfo.java index e73aed890..b7f45915a 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/MovieInfo.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/MovieInfo.java @@ -1,19 +1,27 @@ package org.nzbhydra.mediainfo; import com.google.common.base.Strings; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; -import javax.persistence.*; import java.util.Optional; @Data +@ReflectionMarker @Entity @Table(name = "movieinfo") -public class MovieInfo implements Comparable { +public final class MovieInfo implements Comparable { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @SequenceGenerator(allocationSize = 1, name = "MOVIEINFO_SEQ") protected int id; private String imdbId; diff --git a/core/src/main/java/org/nzbhydra/mediainfo/TmdbHandler.java b/core/src/main/java/org/nzbhydra/mediainfo/TmdbHandler.java index fd09f80f7..a1fd2384e 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/TmdbHandler.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/TmdbHandler.java @@ -1,33 +1,32 @@ package org.nzbhydra.mediainfo; -import com.uwetrottmann.tmdb2.entities.FindResults; -import com.uwetrottmann.tmdb2.entities.Movie; -import com.uwetrottmann.tmdb2.entities.MovieResultsPage; -import com.uwetrottmann.tmdb2.enumerations.ExternalSource; +import org.nzbhydra.Jackson; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.webaccess.WebAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import retrofit2.Call; -import retrofit2.Response; import java.io.IOException; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoField; -import java.util.Collections; +import java.time.LocalDate; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; +@SuppressWarnings("unchecked") @Component public class TmdbHandler { private static final Logger logger = LoggerFactory.getLogger(TmdbHandler.class); - @Autowired - protected CustomTmdb tmdb; + + @Value("${nzbhydra.tmdb.apikey:}") + protected String tmdbApiKey; @Autowired private ConfigProvider configProvider; + @Autowired + private WebAccess webAccess; public TmdbSearchResult getInfos(String value, MediaIdType idType) throws InfoProviderException { @@ -35,99 +34,90 @@ public class TmdbHandler { return fromTitle(value, null); } if (idType == MediaIdType.IMDB) { - return fromImdb(value); + return getMovieByImdbId(value); } if (idType == MediaIdType.TMDB) { - return fromTmdb(value); + return getMovieByTmdbId(value); } throw new IllegalArgumentException("Unable to get infos from " + idType); } - private TmdbSearchResult fromImdb(String imdbId) throws InfoProviderException { - Movie movie = getMovieByImdbId(imdbId); - TmdbSearchResult result = getSearchResultFromMovie(movie); - return result; - } - - private TmdbSearchResult fromTmdb(String tmdbId) throws InfoProviderException { - Movie movie = getMovieByTmdbId(tmdbId); - TmdbSearchResult result = getSearchResultFromMovie(movie); - return result; - } - TmdbSearchResult fromTitle(String title, Integer year) throws InfoProviderException { - Movie movie = getMovieByTitle(title, year); - TmdbSearchResult result = getSearchResultFromMovie(movie); - return result; - } - - private TmdbSearchResult getSearchResultFromMovie(Movie movie) { - String fullPosterUrl = movie.poster_path != null ? ("https://image.tmdb.org/t/p/w500/" + movie.poster_path) : null; - Integer year = movie.release_date != null ? LocalDateTime.ofInstant(movie.release_date.toInstant(), ZoneId.systemDefault()).get(ChronoField.YEAR) : null; - return new TmdbSearchResult(String.valueOf(movie.id), movie.imdb_id, movie.title, fullPosterUrl, year); - } - - private Movie getMovieByTitle(String title, Integer year) throws InfoProviderException { - List movies = search(title, year); - TmdbSearchResult movie = movies.get(0); - //Unfortunately IMDB ID is not filled here, so we need to make a new query using the TMDB ID - return getMovieByTmdbId(String.valueOf(movie.getTmdbId())); + final List list = search(title, year); + return list.isEmpty() ? null : list.get(0); } public List search(String title, Integer year) throws InfoProviderException { - List movies; - Call movieSearch = tmdb.searchService().movie(title, null, configProvider.getBaseConfig().getSearching().getLanguage().orElse("en"), null, year, null, null); + String url = "https://api.themoviedb.org/3/search/movie?query=%s&year=%s&api_key=%s".formatted(title, year == null ? "null" : year, tmdbApiKey); try { - Response response = movieSearch.execute(); - if (!response.isSuccessful()) { - throw new InfoProviderException("Error while contacting TMDB: " + response.errorBody().string()); - } - if (response.body().total_results == 0) { - logger.info("TMDB query for title '{}' returned no searchResults", title); - return Collections.emptyList(); - } - movies = response.body().results; - } catch (IOException e) { - logger.error("Error while contacting TMDB", e); - return Collections.emptyList(); - } + final String json = webAccess.callUrl(url); + final Map map = Jackson.JSON_MAPPER.readValue(json, Map.class); + List list = (List) map.get("results"); + return list.stream() + .limit(10) + .map(x -> { + TmdbSearchResult result = new TmdbSearchResult(); + fillFromMap(x, result); + return result; + }).toList(); - return movies.stream().map(this::getSearchResultFromMovie).collect(Collectors.toList()); + } catch (IOException e) { + throw new InfoProviderException("Error loading details for movie with title " + title, e); + } } - private Movie getMovieByImdbId(String imdbId) throws InfoProviderException { - Movie movie; - Call resultsCall = tmdb.findService().find(imdbId, ExternalSource.IMDB_ID, configProvider.getBaseConfig().getSearching().getLanguage().orElse("en")); + private TmdbSearchResult getMovieByImdbId(String imdbId) throws InfoProviderException { + final String correctImdbId = imdbId.startsWith("tt") ? imdbId : "tt" + imdbId; + String url = "https://api.themoviedb.org/3/find/%s?external_source=imdb_id&api_key=%s".formatted(correctImdbId, tmdbApiKey); try { - Response response = resultsCall.execute(); - if (!response.isSuccessful()) { - throw new InfoProviderException("Error while contacting TMDB: " + response.errorBody().string()); - } - if (response.body().movie_results.size() == 0) { + final String json = webAccess.callUrl(url); + final Map map = Jackson.JSON_MAPPER.readValue(json, Map.class); + final List list = (List) map.get("movie_results"); + TmdbSearchResult result = new TmdbSearchResult(); + if (list.isEmpty()) { throw new InfoProviderException(String.format("TMDB query for IMDB ID %s returned no searchResults", imdbId)); } - movie = response.body().movie_results.get(0); + fillFromMap(list.get(0), result); + result.setImdbId(correctImdbId); + return result; } catch (IOException e) { - throw new InfoProviderException("Error while contacting TMDB", e); + throw new InfoProviderException("Error loading details for movie with IMDB ID " + imdbId, e); } - return movie; } - private Movie getMovieByTmdbId(String tmdbId) throws InfoProviderException { - Movie movie; - Call movieCall = tmdb.moviesService().summary(Integer.valueOf(tmdbId), configProvider.getBaseConfig().getSearching().getLanguage().orElse("en"), null); + private TmdbSearchResult getMovieByTmdbId(String tmdbId) throws InfoProviderException { + String url = "https://api.themoviedb.org/3/movie/%s?api_key=%s".formatted(tmdbId, tmdbApiKey); try { - Response response = movieCall.execute(); - if (!response.isSuccessful()) { - throw new InfoProviderException("Error while contacting TMDB: " + response.errorBody().string()); - } - - movie = response.body(); + final String json = webAccess.callUrl(url); + final Map map = Jackson.JSON_MAPPER.readValue(json, Map.class); + TmdbSearchResult result = new TmdbSearchResult(); + fillFromMap(map, result); + result.setImdbId(getImdbId(tmdbId)); + return result; } catch (IOException e) { - throw new InfoProviderException("Error while contacting TMDB", e); + throw new InfoProviderException("Error loading details for movie with TMDB ID " + tmdbId, e); } - return movie; + } + private static void fillFromMap(Map map, TmdbSearchResult result) { + result.setTmdbId(String.valueOf(map.get("id"))); + result.setYear(map.get("release_date") != null ? LocalDate.parse(map.get("release_date").toString()).getYear() : null); + result.setTitle(String.valueOf(map.get("title"))); + result.setPosterUrl(map.get("poster_path") != null ? ("https://image.tmdb.org/t/p/w500/" + map.get("poster_path")) : null); + } + + private String getImdbId(String tmdbId) throws InfoProviderException { + String url = "https://api.themoviedb.org/3/movie/%s/external_ids?api_key=%s".formatted(tmdbId, tmdbApiKey); + try { + final String json = webAccess.callUrl(url); + final Map map = Jackson.JSON_MAPPER.readValue(json, Map.class); + return String.valueOf(map.get("imdb_id")); + } catch (IOException e) { + throw new InfoProviderException("Error loading details for movie with ID " + tmdbId, e); + } + } + + } diff --git a/core/src/main/java/org/nzbhydra/mediainfo/TmdbSearchResult.java b/core/src/main/java/org/nzbhydra/mediainfo/TmdbSearchResult.java index 98d25978e..67c1a7f39 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/TmdbSearchResult.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/TmdbSearchResult.java @@ -3,8 +3,10 @@ package org.nzbhydra.mediainfo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor class TmdbSearchResult { diff --git a/core/src/main/java/org/nzbhydra/mediainfo/TvInfo.java b/core/src/main/java/org/nzbhydra/mediainfo/TvInfo.java index ffe869f50..05fb24981 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/TvInfo.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/TvInfo.java @@ -1,18 +1,27 @@ package org.nzbhydra.mediainfo; import com.google.common.base.Strings; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; -import javax.persistence.*; import java.util.Optional; @Data +@ReflectionMarker @Entity @Table(name = "tvinfo") -public class TvInfo implements Comparable { +public final class TvInfo implements Comparable { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @SequenceGenerator(allocationSize = 1, name = "TVINFO_SEQ") protected int id; @Column(unique = true) diff --git a/core/src/main/java/org/nzbhydra/mediainfo/TvMazeHandler.java b/core/src/main/java/org/nzbhydra/mediainfo/TvMazeHandler.java index 0f3cf5d40..5bba53886 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/TvMazeHandler.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/TvMazeHandler.java @@ -2,6 +2,8 @@ package org.nzbhydra.mediainfo; import com.google.common.base.MoreObjects; import lombok.Data; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -31,24 +33,15 @@ public class TvMazeHandler { return fromTitle(id); } logger.info("Searching TVMaze for show with {} {}", idType, id); + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("https://api.tvmaze.com/"); - switch (idType) { - case TVRAGE: - builder = builder.pathSegment("lookup", "shows").queryParam("tvrage", id); - break; - case TVDB: - builder = builder.pathSegment("lookup", "shows").queryParam("thetvdb", id); - break; - case TVIMDB: - case IMDB: - builder = builder.pathSegment("lookup", "shows").queryParam("imdb", id.startsWith("tt") ? id : "tt" + id); - break; - case TVMAZE: - builder = builder.pathSegment("shows", id); - break; - default: - throw new InfoProviderException("Unable to handle " + idType); - } + builder = switch (idType) { + case TVRAGE -> builder.pathSegment("lookup", "shows").queryParam("tvrage", id); + case TVDB -> builder.pathSegment("lookup", "shows").queryParam("thetvdb", id); + case TVIMDB, IMDB -> builder.pathSegment("lookup", "shows").queryParam("imdb", id.startsWith("tt") ? id : "tt" + id); + case TVMAZE -> builder.pathSegment("shows", id); + default -> throw new InfoProviderException("Unable to handle " + idType); + }; ResponseEntity showLookupResponse; try { @@ -89,7 +82,7 @@ public class TvMazeHandler { private List searchByTitle(String title) throws InfoProviderException { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("https://api.tvmaze.com/search/shows").queryParam("q", title); - ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>() { + ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() { }; ResponseEntity> lookupResponse = restTemplate.exchange(builder.build().encode().toUri(), HttpMethod.GET, null, typeRef); if (!lookupResponse.getStatusCode().is2xxSuccessful()) { @@ -118,12 +111,14 @@ public class TvMazeHandler { @Data +@ReflectionMarker private static class TvmazeShowSearch { //Without static deserialization fails private Integer score; private TvmazeShow show; } @Data +@ReflectionMarker private static class TvmazeShow { //Without static deserialization fails private Integer id; private String name; @@ -147,6 +142,7 @@ public class TvMazeHandler { } @Data +@ReflectionMarker private static class TvmazeExternals { //Without static deserialization fails private String tvrage; private String thetvdb; @@ -163,6 +159,7 @@ public class TvMazeHandler { } @Data +@ReflectionMarker private static class TvmazeImage { //Without static deserialization fails private String medium; private String original; diff --git a/core/src/main/java/org/nzbhydra/mediainfo/TvMazeSearchResult.java b/core/src/main/java/org/nzbhydra/mediainfo/TvMazeSearchResult.java index 7492b29a0..d16277fd9 100644 --- a/core/src/main/java/org/nzbhydra/mediainfo/TvMazeSearchResult.java +++ b/core/src/main/java/org/nzbhydra/mediainfo/TvMazeSearchResult.java @@ -4,8 +4,10 @@ import com.google.common.base.MoreObjects; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor class TvMazeSearchResult { diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Auth.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Auth.java index bde75f069..5a91ee223 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Auth.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Auth.java @@ -4,12 +4,14 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.ArrayList; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class Auth { @JsonProperty("authType") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Categories.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Categories.java index dd58b43be..7536169bb 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Categories.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Categories.java @@ -4,12 +4,14 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.HashMap; import java.util.Map; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class Categories { @JsonProperty("categories") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Category.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Category.java index c1191c1bf..552416cdc 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Category.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Category.java @@ -5,12 +5,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.ArrayList; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class Category { @JsonProperty("applyRestrictions") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Downloader.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Downloader.java index 89eb3019f..82a7db978 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Downloader.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Downloader.java @@ -4,9 +4,11 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class Downloader { @JsonProperty("defaultCategory") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Indexer.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Indexer.java index 7eb2eda04..c34e5e9f8 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Indexer.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Indexer.java @@ -4,12 +4,14 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.ArrayList; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class Indexer { @JsonProperty("accessType") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/ListOrStringToStringDeserializer.java b/core/src/main/java/org/nzbhydra/migration/configmapping/ListOrStringToStringDeserializer.java index 40e71aa62..2caff8981 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/ListOrStringToStringDeserializer.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/ListOrStringToStringDeserializer.java @@ -21,8 +21,7 @@ public class ListOrStringToStringDeserializer extends JsonDeserializer(); } - if (value instanceof String && !Strings.isNullOrEmpty((String) value)) { - String string = (String) value; + if (value instanceof String string && !Strings.isNullOrEmpty((String) value)) { List result = new ArrayList<>(); if (!string.contains(",")) { result.add(string); diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Logging.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Logging.java index d6fba26b2..ce6df962f 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Logging.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Logging.java @@ -4,9 +4,11 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class Logging { @JsonProperty("consolelevel") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Main.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Main.java index 039ad1cdb..cf2cc8843 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Main.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Main.java @@ -4,9 +4,11 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class Main { @JsonProperty("apikey") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/OldConfig.java b/core/src/main/java/org/nzbhydra/migration/configmapping/OldConfig.java index 7771a905e..8d654b695 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/OldConfig.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/OldConfig.java @@ -4,12 +4,14 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.ArrayList; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class OldConfig { @JsonProperty("auth") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/Searching.java b/core/src/main/java/org/nzbhydra/migration/configmapping/Searching.java index ce96e6e69..44bc541a6 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/Searching.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/Searching.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.List; @@ -33,6 +34,7 @@ import java.util.List; "userAgent" }) @Data +@ReflectionMarker public class Searching { @JsonProperty("applyRestrictions") diff --git a/core/src/main/java/org/nzbhydra/migration/configmapping/User.java b/core/src/main/java/org/nzbhydra/migration/configmapping/User.java index d3e5f150a..c34616da3 100644 --- a/core/src/main/java/org/nzbhydra/migration/configmapping/User.java +++ b/core/src/main/java/org/nzbhydra/migration/configmapping/User.java @@ -4,9 +4,11 @@ package org.nzbhydra.migration.configmapping; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; @JsonInclude(JsonInclude.Include.NON_NULL) @Data +@ReflectionMarker public class User { @JsonProperty("maySeeAdmin") diff --git a/core/src/main/java/org/nzbhydra/misc/OpenPortChecker.java b/core/src/main/java/org/nzbhydra/misc/OpenPortChecker.java index 0b105603f..2c2cea6c9 100644 --- a/core/src/main/java/org/nzbhydra/misc/OpenPortChecker.java +++ b/core/src/main/java/org/nzbhydra/misc/OpenPortChecker.java @@ -16,16 +16,17 @@ package org.nzbhydra.misc; +import com.gargoylesoftware.htmlunit.BrowserVersion; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.HtmlButton; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlTextInput; +import jakarta.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import javax.annotation.Nullable; import java.io.IOException; @Component @@ -37,8 +38,9 @@ public class OpenPortChecker { if (ip == null) { ip = getPublicIp(); } - try (final WebClient webClient = new WebClient()) { + try (final WebClient webClient = new WebClient(BrowserVersion.getDefault(), false, null,-1)) { webClient.getOptions().setThrowExceptionOnScriptError(false); + webClient.getOptions().setCssEnabled(false); final HtmlPage page1 = webClient.getPage("https://portchecker.co/check"); diff --git a/core/src/main/java/org/nzbhydra/misc/WebHooks.java b/core/src/main/java/org/nzbhydra/misc/WebHooks.java index 6d1d7d46d..84f690e60 100644 --- a/core/src/main/java/org/nzbhydra/misc/WebHooks.java +++ b/core/src/main/java/org/nzbhydra/misc/WebHooks.java @@ -7,11 +7,11 @@ import okhttp3.Request.Builder; import okhttp3.RequestBody; import okhttp3.Response; import org.nzbhydra.Jackson; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.downloading.FileDownloadEntity; import org.nzbhydra.downloading.FileDownloadEvent; import org.nzbhydra.downloading.FileDownloadRepository; import org.nzbhydra.searching.Searcher.SearchEvent; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +41,7 @@ public class WebHooks { if (!Strings.isNullOrEmpty(searchHook)) { if (searchEvent.getSearchRequest().getSource() == SearchSource.INTERNAL) { try { - OkHttpClient client = requestFactory.getOkHttpClientBuilder(URI.create(searchHook)).build(); + OkHttpClient client = requestFactory.getOkHttpClient(URI.create(searchHook).getHost()); String content = Jackson.JSON_MAPPER.writeValueAsString(searchEvent.getSearchRequest()); Response response = client.newCall(new Builder().url(searchHook).method("PUT", RequestBody.create(MediaType.parse(org.springframework.http.MediaType.APPLICATION_JSON_VALUE), content)).build()).execute(); response.close(); @@ -63,7 +63,7 @@ public class WebHooks { FileDownloadEntity downloadEntity = downloadEvent.getFileDownloadEntity(); if (downloadEntity.getAccessSource() == SearchSource.INTERNAL) { try { - OkHttpClient client = requestFactory.getOkHttpClientBuilder(URI.create(downloadHook)).build(); + OkHttpClient client = requestFactory.getOkHttpClient(URI.create(downloadHook).getHost()); String content = Jackson.JSON_MAPPER.writeValueAsString(downloadEntity); Response response = client.newCall(new Builder().url(downloadHook).method("PUT", RequestBody.create(MediaType.parse(org.springframework.http.MediaType.APPLICATION_JSON_VALUE), content)).build()).execute(); response.close(); diff --git a/core/src/main/java/org/nzbhydra/news/NewsProvider.java b/core/src/main/java/org/nzbhydra/news/NewsProvider.java index f42e2bb12..a065bf3ba 100644 --- a/core/src/main/java/org/nzbhydra/news/NewsProvider.java +++ b/core/src/main/java/org/nzbhydra/news/NewsProvider.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.mapping.SemanticVersion; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.update.UpdateManager; import org.nzbhydra.webaccess.WebAccess; import org.slf4j.Logger; @@ -40,7 +41,7 @@ public class NewsProvider { public List getNews() throws IOException { if (Instant.now().minus(2, ChronoUnit.HOURS).isAfter(lastCheckedForNews)) { - newsEntries = webAccess.callUrl(newsUrl, new TypeReference>() { + newsEntries = webAccess.callUrl(newsUrl, new TypeReference<>() { }); newsEntries.sort(Comparator.comparing(NewsEntry::getShowForVersion).reversed()); lastCheckedForNews = Instant.now(); @@ -77,6 +78,7 @@ public class NewsProvider { @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class NewsEntry { diff --git a/core/src/main/java/org/nzbhydra/news/NewsWeb.java b/core/src/main/java/org/nzbhydra/news/NewsWeb.java index 0386d7d2a..a872b0fd1 100644 --- a/core/src/main/java/org/nzbhydra/news/NewsWeb.java +++ b/core/src/main/java/org/nzbhydra/news/NewsWeb.java @@ -1,8 +1,6 @@ package org.nzbhydra.news; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import jakarta.servlet.http.HttpSession; import org.nzbhydra.ExceptionInfo; import org.nzbhydra.GenericResponse; import org.nzbhydra.Markdown; @@ -23,7 +21,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.WebRequest; -import javax.servlet.http.HttpSession; import java.io.IOException; import java.security.Principal; import java.util.ArrayList; @@ -83,19 +80,11 @@ public class NewsWeb { for (NewsEntry entry : entries) { boolean isForCurrentVersion = entry.getShowForVersion().equals(new SemanticVersion(updateManager.getCurrentVersionString())); boolean isForNewerVersion = entry.getShowForVersion().isUpdateFor(new SemanticVersion(updateManager.getCurrentVersionString())); - transformedEntries.add(new NewsEntryForWeb(entry.getShowForVersion().getAsString(), Markdown.renderMarkdownAsHtml(entry.getNewsAsMarkdown()), isForCurrentVersion, isForNewerVersion)); + final String newsAsMarkdown = entry.getNewsAsMarkdown(); + transformedEntries.add(new NewsEntryForWeb(entry.getShowForVersion().getAsString(), Markdown.renderMarkdownAsHtml(newsAsMarkdown), isForCurrentVersion, isForNewerVersion)); } return transformedEntries; } - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class NewsEntryForWeb { - private String version; - private String news; - private boolean forCurrentVersion; - private boolean forNewerVersion; - } } diff --git a/core/src/main/java/org/nzbhydra/news/ShownNews.java b/core/src/main/java/org/nzbhydra/news/ShownNews.java index 9f702548a..b8efb7e44 100644 --- a/core/src/main/java/org/nzbhydra/news/ShownNews.java +++ b/core/src/main/java/org/nzbhydra/news/ShownNews.java @@ -1,16 +1,23 @@ package org.nzbhydra.news; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; - -import javax.persistence.*; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @Entity @Table(name = "shownnews") -public class ShownNews { +public final class ShownNews { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @SequenceGenerator(allocationSize = 1, name = "SHOWNNEWS_SEQ") protected int id; private String version; diff --git a/core/src/main/java/org/nzbhydra/notifications/AuthFailureNotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/AuthFailureNotificationEvent.java index 45191f1a0..65f6a7921 100644 --- a/core/src/main/java/org/nzbhydra/notifications/AuthFailureNotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/AuthFailureNotificationEvent.java @@ -19,11 +19,14 @@ package org.nzbhydra.notifications; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.HashMap; import java.util.Map; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class AuthFailureNotificationEvent implements NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/notifications/DownloadCompletionNotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/DownloadCompletionNotificationEvent.java index 1e3f6bd65..69f2b71dc 100644 --- a/core/src/main/java/org/nzbhydra/notifications/DownloadCompletionNotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/DownloadCompletionNotificationEvent.java @@ -19,11 +19,14 @@ package org.nzbhydra.notifications; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.HashMap; import java.util.Map; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class DownloadCompletionNotificationEvent implements NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/notifications/DownloadNotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/DownloadNotificationEvent.java index c6e57f033..bcb079228 100644 --- a/core/src/main/java/org/nzbhydra/notifications/DownloadNotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/DownloadNotificationEvent.java @@ -19,11 +19,14 @@ package org.nzbhydra.notifications; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.HashMap; import java.util.Map; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class DownloadNotificationEvent implements NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/notifications/IndexerDisabledNotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/IndexerDisabledNotificationEvent.java index 4e172930a..7f07cf013 100644 --- a/core/src/main/java/org/nzbhydra/notifications/IndexerDisabledNotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/IndexerDisabledNotificationEvent.java @@ -20,11 +20,14 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.HashMap; import java.util.Map; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class IndexerDisabledNotificationEvent implements NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/notifications/IndexerReenabledNotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/IndexerReenabledNotificationEvent.java index b6bbb9039..18e5b8943 100644 --- a/core/src/main/java/org/nzbhydra/notifications/IndexerReenabledNotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/IndexerReenabledNotificationEvent.java @@ -19,12 +19,15 @@ package org.nzbhydra.notifications; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; import java.time.Instant; import java.util.HashMap; import java.util.Map; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class IndexerReenabledNotificationEvent implements NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/notifications/IndexerVipExpiryNotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/IndexerVipExpiryNotificationEvent.java index 72158c055..d151ea0cb 100644 --- a/core/src/main/java/org/nzbhydra/notifications/IndexerVipExpiryNotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/IndexerVipExpiryNotificationEvent.java @@ -19,11 +19,14 @@ package org.nzbhydra.notifications; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.HashMap; import java.util.Map; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class IndexerVipExpiryNotificationEvent implements NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/notifications/NotificationEntity.java b/core/src/main/java/org/nzbhydra/notifications/NotificationEntity.java index f84cf7134..270de4aaf 100644 --- a/core/src/main/java/org/nzbhydra/notifications/NotificationEntity.java +++ b/core/src/main/java/org/nzbhydra/notifications/NotificationEntity.java @@ -1,38 +1,36 @@ package org.nzbhydra.notifications; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; import java.time.Instant; @Data +@ReflectionMarker @Entity @Table(name = "notification") -public class NotificationEntity { - - public enum MessageType { - INFO, - SUCCESS, - WARNING, - FAILURE - } +public final class NotificationEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @SequenceGenerator(allocationSize = 1, name = "NOTIFICATION_SEQ") protected int id; @Enumerated(EnumType.STRING) private NotificationEventType notificationEventType; @Enumerated(EnumType.STRING) - private MessageType messageType; + private NotificationMessageType messageType; private String title; private String body; @@ -46,7 +44,7 @@ public class NotificationEntity { public NotificationEntity() { } - public NotificationEntity(NotificationEventType notificationEventType, MessageType messageType, String title, String body, String urls, Instant time) { + public NotificationEntity(NotificationEventType notificationEventType, NotificationMessageType messageType, String title, String body, String urls, Instant time) { this.notificationEventType = notificationEventType; this.title = title; this.body = body; diff --git a/core/src/main/java/org/nzbhydra/notifications/NotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/NotificationEvent.java index e51f6311e..8abf2eb2f 100644 --- a/core/src/main/java/org/nzbhydra/notifications/NotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/NotificationEvent.java @@ -16,6 +16,8 @@ package org.nzbhydra.notifications; +import org.nzbhydra.config.notification.NotificationEventType; + import java.util.Map; public interface NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/notifications/NotificationHandler.java b/core/src/main/java/org/nzbhydra/notifications/NotificationHandler.java index ab17db388..e5c4624c1 100644 --- a/core/src/main/java/org/nzbhydra/notifications/NotificationHandler.java +++ b/core/src/main/java/org/nzbhydra/notifications/NotificationHandler.java @@ -28,6 +28,7 @@ import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.NotificationConfig; import org.nzbhydra.config.NotificationConfigEntry; import org.nzbhydra.logging.LoggingMarkers; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.webaccess.WebAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,7 +45,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Pattern; -import java.util.stream.Collectors; @Component public class NotificationHandler { @@ -65,8 +65,8 @@ public class NotificationHandler { final NotificationConfig notificationConfig = configProvider.getBaseConfig().getNotificationConfig(); final List configEntries = notificationConfig.getEntries().stream() - .filter(x -> x.getEventType() == event.getEventType()) - .collect(Collectors.toList()); + .filter(x -> x.getEventType() == event.getEventType()) + .toList(); if (configEntries.isEmpty()) { logger.debug(LoggingMarkers.NOTIFICATIONS, "No matching config entries found"); @@ -111,7 +111,7 @@ public class NotificationHandler { throw new RuntimeException("Unable to generate notification body", e); } - notificationRepository.save(new NotificationEntity(event.getEventType(), NotificationEntity.MessageType.valueOf(configEntry.getMessageType().name()), notificationTitle, notificationBody, configEntry.getAppriseUrls(), Instant.now())); + notificationRepository.save(new NotificationEntity(event.getEventType(), NotificationMessageType.valueOf(configEntry.getMessageType().name()), notificationTitle, notificationBody, configEntry.getAppriseUrls(), Instant.now())); if (notificationConfig.getAppriseType() == NotificationConfig.AppriseType.NONE) { logger.debug(LoggingMarkers.NOTIFICATIONS, "Apprise type set to None"); @@ -176,6 +176,7 @@ public class NotificationHandler { } @Data +@ReflectionMarker @AllArgsConstructor private static class AppriseMessage { diff --git a/core/src/main/java/org/nzbhydra/notifications/NotificationRepository.java b/core/src/main/java/org/nzbhydra/notifications/NotificationRepository.java index 90e3052e8..96cb92a5f 100644 --- a/core/src/main/java/org/nzbhydra/notifications/NotificationRepository.java +++ b/core/src/main/java/org/nzbhydra/notifications/NotificationRepository.java @@ -1,6 +1,7 @@ package org.nzbhydra.notifications; +import org.nzbhydra.config.notification.NotificationEventType; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; diff --git a/core/src/main/java/org/nzbhydra/notifications/NotificationsWeb.java b/core/src/main/java/org/nzbhydra/notifications/NotificationsWeb.java index d5ae0bdca..3b54c918d 100644 --- a/core/src/main/java/org/nzbhydra/notifications/NotificationsWeb.java +++ b/core/src/main/java/org/nzbhydra/notifications/NotificationsWeb.java @@ -17,7 +17,8 @@ package org.nzbhydra.notifications; import com.google.common.collect.Sets; -import org.nzbhydra.ShutdownEvent; +import jakarta.annotation.PreDestroy; +import org.nzbhydra.config.notification.NotificationEventType; import org.nzbhydra.logging.LoggingMarkers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,7 +84,7 @@ public class NotificationsWeb { @MessageMapping("/markNotificationRead") public void markRead(int id) { final Optional byId = notificationRepository.findById(id); - if (!byId.isPresent()) { + if (byId.isEmpty()) { logger.error("Unable to mark notification with ID {} as read because no notification with that ID was found", id); return; } @@ -101,7 +102,7 @@ public class NotificationsWeb { .stream() .filter(x -> x != null && x.getEventType() == notificationEventType) .findFirst(); - if (!notificationEvent.isPresent()) { + if (notificationEvent.isEmpty()) { throw new RuntimeException("Unable to create test notification for event type " + eventType); } logger.info("Sending test notification for type {}", eventType); @@ -145,8 +146,8 @@ public class NotificationsWeb { } } - @EventListener - public void onShutdown(ShutdownEvent event) { + @PreDestroy + public void onShutdown() { if (scheduledFuture != null) { scheduledFuture.cancel(true); scheduledFuture = null; diff --git a/core/src/main/java/org/nzbhydra/notifications/UpdateNotificationEvent.java b/core/src/main/java/org/nzbhydra/notifications/UpdateNotificationEvent.java index 71e0a1462..c9c75a888 100644 --- a/core/src/main/java/org/nzbhydra/notifications/UpdateNotificationEvent.java +++ b/core/src/main/java/org/nzbhydra/notifications/UpdateNotificationEvent.java @@ -19,11 +19,14 @@ package org.nzbhydra.notifications; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.config.notification.NotificationEventType; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.HashMap; import java.util.Map; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class UpdateNotificationEvent implements NotificationEvent { diff --git a/core/src/main/java/org/nzbhydra/problemdetection/DeleteOldDatabaseBackupDetector.java b/core/src/main/java/org/nzbhydra/problemdetection/DeleteOldDatabaseBackupDetector.java new file mode 100644 index 000000000..4fd33620d --- /dev/null +++ b/core/src/main/java/org/nzbhydra/problemdetection/DeleteOldDatabaseBackupDetector.java @@ -0,0 +1,56 @@ +/* + * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.problemdetection; + +import org.apache.commons.io.filefilter.WildcardFileFilter; +import org.nzbhydra.NzbHydra; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +@Component +public class DeleteOldDatabaseBackupDetector implements ProblemDetector { + + private static final Logger logger = LoggerFactory.getLogger(DeleteOldDatabaseBackupDetector.class); + + + @Override + public void executeCheck() { + File databaseFolder = new File(NzbHydra.getDataFolder(), "database"); + if (!databaseFolder.exists()) { + return; + } + final String[] files = databaseFolder.list(new WildcardFileFilter("*.old.bak.*")); + if (files == null) { + return; + } + for (String file : files) { + final String timeMilis = file.substring(file.lastIndexOf(".") + 1); + final Instant creationTime = Instant.ofEpochMilli(Long.parseLong(timeMilis)); + if (creationTime.isBefore(Instant.now().minus(14, ChronoUnit.DAYS))) { + logger.info("Found old database migration backup {}. Deleting it", file); + new File(databaseFolder, file).delete(); + } + } + } + + +} diff --git a/core/src/main/java/org/nzbhydra/problemdetection/OutOfMemoryDetector.java b/core/src/main/java/org/nzbhydra/problemdetection/OutOfMemoryDetector.java index b864adfdf..2aedbdc18 100644 --- a/core/src/main/java/org/nzbhydra/problemdetection/OutOfMemoryDetector.java +++ b/core/src/main/java/org/nzbhydra/problemdetection/OutOfMemoryDetector.java @@ -23,9 +23,12 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; @Component public class OutOfMemoryDetector implements ProblemDetector { @@ -50,30 +53,33 @@ public class OutOfMemoryDetector implements ProblemDetector { state.set(State.LOOKING_FOR_OOM); AtomicReference lastTimeStampLine = new AtomicReference<>(); - Files.lines(logContentProvider.getCurrentLogfile(false).toPath()).forEach(line -> { - if (line.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.*")) { - if (state.get() == State.LOOKING_FOR_OOM) { - lastTimeStampLine.set(line); - } - if (state.get() == State.LOOKING_FOR_OOM_END) { - state.set(State.LOOKING_FOR_OOM); - } - return; - } - - if (line.contains("java.lang.OutOfMemoryError")) { - if (state.get() == State.LOOKING_FOR_OOM) { - String key = "outOfMemoryDetected-" + lastTimeStampLine.get(); - boolean alreadyDetected = genericStorage.get(key, String.class).isPresent(); - if (!alreadyDetected) { - logger.warn("The log indicates that the process ran out of memory. Please increase the XMX value in the main config and restart."); - genericStorage.save(key, true); - genericStorage.save("outOfMemoryDetected", true); + File currentLogfile = logContentProvider.getCurrentLogfile(false); + try (Stream lines = Files.lines(currentLogfile.toPath(), StandardCharsets.ISO_8859_1)) { + lines.forEach(line -> { + if (line.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.*")) { + if (state.get() == State.LOOKING_FOR_OOM) { + lastTimeStampLine.set(line); } - state.set(State.LOOKING_FOR_OOM_END); + if (state.get() == State.LOOKING_FOR_OOM_END) { + state.set(State.LOOKING_FOR_OOM); + } + return; } - } - }); + + if (line.contains("java.lang.OutOfMemoryError")) { + if (state.get() == State.LOOKING_FOR_OOM) { + String key = "outOfMemoryDetected-" + lastTimeStampLine.get(); + boolean alreadyDetected = genericStorage.get(key, String.class).isPresent(); + if (!alreadyDetected) { + logger.warn("The log indicates that the process ran out of memory. Please increase the XMX value in the main config and restart."); + genericStorage.save(key, true); + genericStorage.save("outOfMemoryDetected", true); + } + state.set(State.LOOKING_FOR_OOM_END); + } + } + }); + } } catch (IOException e) { logger.warn("Unable to read log file: " + e.getMessage()); diff --git a/core/src/main/java/org/nzbhydra/problemdetection/OutdatedWrapperDetector.java b/core/src/main/java/org/nzbhydra/problemdetection/OutdatedWrapperDetector.java index fc3d91ebd..261332317 100644 --- a/core/src/main/java/org/nzbhydra/problemdetection/OutdatedWrapperDetector.java +++ b/core/src/main/java/org/nzbhydra/problemdetection/OutdatedWrapperDetector.java @@ -31,6 +31,7 @@ import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,7 +66,7 @@ public class OutdatedWrapperDetector implements ProblemDetector { List wrapperFilenames = Arrays.asList("NZBHydra2.exe", "NZBHydra2 Console.exe", "nzbhydra2", "nzbhydra2wrapper.py", "nzbhydra2wrapperPy3.py"); Map filenamesToExpectedHashes; try { - filenamesToExpectedHashes = Jackson.JSON_MAPPER.readValue(OutdatedWrapperDetector.class.getResource("/wrapperHashes.json"), new TypeReference>() { + filenamesToExpectedHashes = Jackson.JSON_MAPPER.readValue(OutdatedWrapperDetector.class.getResource("/wrapperHashes2.json"), new TypeReference<>() { }); } catch (IOException e) { logger.error("Error while trying to read wrapper hashes", e); @@ -82,7 +83,7 @@ public class OutdatedWrapperDetector implements ProblemDetector { boolean outdatedWrapperFound = false; if (wrapperFile.exists()) { try { - HashCode hash = Files.hash(wrapperFile, Hashing.sha1()); + HashCode hash = Files.asByteSource(wrapperFile).hash( Hashing.sha1()); final String expectedHash = filenamesToExpectedHashes.get(filename); final String actualHash = hash.toString(); if (!expectedHash.equals(actualHash)) { @@ -97,7 +98,7 @@ public class OutdatedWrapperDetector implements ProblemDetector { genericStorage.save(KEY_OUTDATED_WRAPPER_DETECTED_WARNING_DISPLAYED, false); genericStorage.save(KEY_OUTDATED_WRAPPER_DETECTED, true); logger.warn("The NZBHydra wrappers (i.e. the executables or python scripts you use to run NZBHydra) seem to be outdated. Please update them:\n" + - "Shut down NZBHydra, download the latest version and extract *all files* into your main NZBHydra folder (overwriting all). Start NZBHydra again."); + "Shut down NZBHydra, download the latest version and extract *all files* into your main NZBHydra folder (overwriting all). Start NZBHydra again."); } else { genericStorage.save(KEY_OUTDATED_WRAPPER_DETECTED, false); } @@ -105,4 +106,19 @@ public class OutdatedWrapperDetector implements ProblemDetector { } } + public static void main(String[] args) throws Exception { + final List files = Arrays.asList(new File("releases/windows-release/include/NZBHydra2.exe"), + new File("releases/windows-release/include/NZBHydra2 Console.exe"), + new File("releases/linux-release/include/nzbhydra2"), + new File("other/wrapper/nzbhydra2wrapper.py"), + new File("other/wrapper/nzbhydra2wrapperPy3.py")); + final Map hashes = new HashMap<>(); + for (File file : files) { + hashes.put(file.getName(), Files.asByteSource(file).hash(Hashing.sha1()).toString()); + } + System.out.println(Jackson.JSON_MAPPER.writeValueAsString(hashes)); + + } + + } diff --git a/core/src/main/java/org/nzbhydra/problemdetection/ProblemDetectorTask.java b/core/src/main/java/org/nzbhydra/problemdetection/ProblemDetectorTask.java index 0bffe1850..2a5afbd7e 100644 --- a/core/src/main/java/org/nzbhydra/problemdetection/ProblemDetectorTask.java +++ b/core/src/main/java/org/nzbhydra/problemdetection/ProblemDetectorTask.java @@ -17,6 +17,7 @@ package org.nzbhydra.problemdetection; import com.google.common.base.Stopwatch; +import jakarta.annotation.PostConstruct; import org.apache.commons.lang3.JavaVersion; import org.apache.commons.lang3.SystemUtils; import org.nzbhydra.debuginfos.DebugInfosProvider; @@ -28,7 +29,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import java.util.List; import java.util.concurrent.TimeUnit; @@ -48,7 +48,6 @@ public class ProblemDetectorTask { @PostConstruct public void init() { //Check on startup if the wrapper has been updated - detectProblems(); if (!DebugInfosProvider.isRunInDocker() && !SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_17)) { genericStorage.save("belowJava17", true); logger.info("Current Java version is below Java 17: " + SystemUtils.JAVA_SPECIFICATION_VERSION); diff --git a/core/src/main/java/org/nzbhydra/problemdetection/VipExpiryDetector.java b/core/src/main/java/org/nzbhydra/problemdetection/VipExpiryDetector.java index f352b1fa0..70fb3c8f0 100644 --- a/core/src/main/java/org/nzbhydra/problemdetection/VipExpiryDetector.java +++ b/core/src/main/java/org/nzbhydra/problemdetection/VipExpiryDetector.java @@ -27,6 +27,7 @@ import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.genericstorage.GenericStorage; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.notifications.IndexerVipExpiryNotificationEvent; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -40,7 +41,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; - @Component public class VipExpiryDetector implements ProblemDetector { @@ -64,17 +64,17 @@ public class VipExpiryDetector implements ProblemDetector { @Override public void executeCheck() { final List indexersSoonExpiring = configProvider.getBaseConfig().getIndexers().stream() - .filter(x -> !Strings.isNullOrEmpty(x.getVipExpirationDate()) && !x.getVipExpirationDate().equals("Lifetime")) - .filter(x -> { - try { - final LocalDate expiryDate = LocalDate.parse(x.getVipExpirationDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd")); - return expiryDate.isBefore(LocalDate.now().plusDays(14)) && expiryDate.isAfter(LocalDate.now()); - } catch (Exception e) { - logger.error("Unable to parse expiry date {}", x.getVipExpirationDate(), e); - return false; - } - }) - .collect(Collectors.toList()); + .filter(x -> !Strings.isNullOrEmpty(x.getVipExpirationDate()) && !x.getVipExpirationDate().equals("Lifetime")) + .filter(x -> { + try { + final LocalDate expiryDate = LocalDate.parse(x.getVipExpirationDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd")); + return expiryDate.isBefore(LocalDate.now().plusDays(14)) && expiryDate.isAfter(LocalDate.now()); + } catch (Exception e) { + logger.error("Unable to parse expiry date {}", x.getVipExpirationDate(), e); + return false; + } + }) + .toList(); logger.debug(LoggingMarkers.VIP_EXPIRY, "Found indexers with VIP expiry data in the next 14 days: {}", indexersSoonExpiring.stream().map(x -> x.getName() + ": " + x.getVipExpirationDate()).collect(Collectors.joining(", "))); @@ -97,6 +97,7 @@ public class VipExpiryDetector implements ProblemDetector { } @Data + @ReflectionMarker @NoArgsConstructor private static class VipExpiryData implements Serializable { @@ -109,6 +110,7 @@ public class VipExpiryDetector implements ProblemDetector { @NoArgsConstructor @EqualsAndHashCode @ToString + @ReflectionMarker private static class VipExpiryDataEntry implements Serializable { private String indexerName; diff --git a/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java b/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java index 9219aae6b..f45079911 100644 --- a/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java +++ b/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java @@ -80,7 +80,7 @@ public class CategoryProvider implements InitializingBean { categoryMap = categories.stream().collect(Collectors.toMap(Category::getName, Function.identity())); categoryMapByNumber.clear(); for (Category category : categories) { - for (Integer integer : category.getNewznabCategories().stream().filter(x -> x.size() == 1).map(x -> x.get(0)).collect(Collectors.toList())) { + for (Integer integer : category.getNewznabCategories().stream().filter(x -> x.size() == 1).map(x -> x.get(0)).toList()) { categoryMapByNumber.put(integer, category); } category.getNewznabCategories().stream().filter(x -> x.size() > 1).forEach(x -> categoryMapByMultipleNumber.put(x, category)); @@ -191,7 +191,7 @@ public class CategoryProvider implements InitializingBean { String catsString = Joiner.on(",").join(cats); //If the list contains a main category always use that one - List foundMainCategories = cats.stream().filter(x -> x % 1000 == 0).collect(Collectors.toList()); + List foundMainCategories = cats.stream().filter(x -> x % 1000 == 0).toList(); if (!foundMainCategories.isEmpty()) { Category category = categoryMapByNumber.get(foundMainCategories.get(0)); if (category != null) { @@ -209,7 +209,7 @@ public class CategoryProvider implements InitializingBean { //No main categories found, specific subcategory must've been supplied result = getMatchingCategoryOrMatchingMainCategory(cats, defaultCategory); } else { - List matchingSubcategories = cats.stream().filter(cat -> categoryMapByNumber.containsKey(cat)).collect(Collectors.toList()); + List matchingSubcategories = cats.stream().filter(cat -> categoryMapByNumber.containsKey(cat)).toList(); if (matchingSubcategories.size() == 1) { result = categoryMapByNumber.get(matchingSubcategories.get(0)); } else if (matchingSubcategories.size() == 0) { @@ -264,7 +264,7 @@ public class CategoryProvider implements InitializingBean { //Let's try to find a more general one Optional found = Optional.empty(); for (Category category : categories) { - List categorySingleNewznabNumbers = category.getNewznabCategories().stream().filter(x -> x.size() == 1).map(x -> x.get(0)).collect(Collectors.toList()); + List categorySingleNewznabNumbers = category.getNewznabCategories().stream().filter(x -> x.size() == 1).map(x -> x.get(0)).toList(); for (Integer cat : cats) { if (categorySingleNewznabNumbers.contains(cat / 1000 * 1000)) { logger.debug(LoggingMarkers.CATEGORY_MAPPING, "Determined {} matching generally {}", cat, category); diff --git a/core/src/main/java/org/nzbhydra/searching/CustomQueryAndTitleMapping.java b/core/src/main/java/org/nzbhydra/searching/CustomQueryAndTitleMappingHandler.java similarity index 63% rename from core/src/main/java/org/nzbhydra/searching/CustomQueryAndTitleMapping.java rename to core/src/main/java/org/nzbhydra/searching/CustomQueryAndTitleMappingHandler.java index cd36ac939..dcec3285d 100644 --- a/core/src/main/java/org/nzbhydra/searching/CustomQueryAndTitleMapping.java +++ b/core/src/main/java/org/nzbhydra/searching/CustomQueryAndTitleMappingHandler.java @@ -16,7 +16,6 @@ package org.nzbhydra.searching; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.Joiner; import lombok.AllArgsConstructor; import lombok.Data; @@ -24,10 +23,13 @@ import lombok.NoArgsConstructor; import org.jetbrains.annotations.Nullable; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.searching.AffectedValue; +import org.nzbhydra.config.searching.CustomQueryAndTitleMapping; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -41,30 +43,21 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Optional; -import java.util.StringJoiner; -import java.util.regex.Pattern; -import java.util.stream.Collectors; @Component @RestController -public class CustomQueryAndTitleMapping { +public class CustomQueryAndTitleMappingHandler { - public enum AffectedValue { - TITLE, - QUERY, - RESULT_TITLE - } - - private static final Logger logger = LoggerFactory.getLogger(CustomQueryAndTitleMapping.class); + private static final Logger logger = LoggerFactory.getLogger(CustomQueryAndTitleMappingHandler.class); @Autowired private ConfigProvider configProvider; - public CustomQueryAndTitleMapping() { + public CustomQueryAndTitleMappingHandler() { } - public CustomQueryAndTitleMapping(BaseConfig baseConfig) { + public CustomQueryAndTitleMappingHandler(BaseConfig baseConfig) { this.configProvider = new ConfigProvider() { @Override public BaseConfig getBaseConfig() { @@ -77,11 +70,11 @@ public class CustomQueryAndTitleMapping { return mapSearchRequest(searchRequest, configProvider.getBaseConfig().getSearching().getCustomMappings()); } - public SearchRequest mapSearchRequest(SearchRequest searchRequest, List mappings) { - if (mappings.isEmpty() || mappings.stream().allMatch(x -> x.getAffectedValue() == AffectedValue.RESULT_TITLE)) { + public SearchRequest mapSearchRequest(SearchRequest searchRequest, List customQueryAndTitleMappings) { + if (customQueryAndTitleMappings.isEmpty() || customQueryAndTitleMappings.stream().allMatch(x -> x.getAffectedValue() == AffectedValue.RESULT_TITLE)) { return searchRequest; } - final MetaData metaData = mapMetaData(MetaData.fromSearchRequest(searchRequest), mappings); + final MetaData metaData = mapMetaData(MetaData.fromSearchRequest(searchRequest), customQueryAndTitleMappings); metaData.getQuery().ifPresent(searchRequest::setQuery); metaData.getTitle().ifPresent(searchRequest::setTitle); @@ -89,17 +82,17 @@ public class CustomQueryAndTitleMapping { } - public SearchResultItem mapSearchResult(SearchResultItem searchResult, List mappings) { - if (mappings.isEmpty() || mappings.stream().noneMatch(x -> x.getAffectedValue() == AffectedValue.RESULT_TITLE)) { + public SearchResultItem mapSearchResult(SearchResultItem searchResult, List customQueryAndTitleMappings) { + if (customQueryAndTitleMappings.isEmpty() || customQueryAndTitleMappings.stream().noneMatch(x -> x.getAffectedValue() == AffectedValue.RESULT_TITLE)) { return searchResult; } - final MetaData metaData = mapMetaData(MetaData.fromSearchResult(searchResult), mappings); + final MetaData metaData = mapMetaData(MetaData.fromSearchResult(searchResult), customQueryAndTitleMappings); metaData.getTitle().ifPresent(searchResult::setTitle); return searchResult; } - public MetaData mapMetaData(MetaData metaData, List mappings) { + public MetaData mapMetaData(MetaData metaData, List customQueryAndTitleMappings) { if (metaData.getQuery().isPresent() && configProvider.getBaseConfig().getSearching().isReplaceUmlauts()) { final String oldQuery = metaData.getQuery().get(); metaData.setQuery(oldQuery @@ -117,33 +110,33 @@ public class CustomQueryAndTitleMapping { } - final List datasets = mappings.stream() - .filter(x -> metaData.getSearchType() == x.searchType || metaData.type == MetaData.Type.RESULT_TITLE) - .filter(mapping -> isDatasetMatch(metaData, mapping)) - .filter(mapping -> { - if (mapping.to.contains("{season:") && !metaData.getSeason().isPresent()) { - logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Can't use mapping {} because no season information is available for {}", mapping, metaData); + final List datasets = customQueryAndTitleMappings.stream() + .filter(x -> metaData.getSearchType() == x.getSearchType() || metaData.type == MetaData.Type.RESULT_TITLE) + .filter(customQueryAndTitleMapping -> isDatasetMatch(metaData, customQueryAndTitleMapping)) + .filter(customQueryAndTitleMapping -> { + if (customQueryAndTitleMapping.getTo().contains("{season:") && metaData.getSeason().isEmpty()) { + logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Can't use customQueryAndTitleMapping {} because no season information is available for {}", customQueryAndTitleMapping, metaData); return false; } - if (mapping.to.contains("{episode:") && !metaData.getEpisode().isPresent()) { - logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Can't use mapping {} because no episode information is available for {}", mapping, metaData); + if (customQueryAndTitleMapping.getTo().contains("{episode:") && metaData.getEpisode().isEmpty()) { + logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Can't use customQueryAndTitleMapping {} because no episode information is available for {}", customQueryAndTitleMapping, metaData); return false; } return true; }) - .collect(Collectors.toList()); + .toList(); if (datasets.isEmpty()) { logger.debug(LoggingMarkers.CUSTOM_MAPPING, "No datasets found matching: {}", metaData); return metaData; } if (datasets.size() > 1) { - logger.error("Unable to map search request ({}) because multiple mappings match it:\n{}", metaData, Joiner.on("\n").join(mappings)); + logger.error("Unable to map search request ({}) because multiple customQueryAndTitleMappings match it:\n{}", metaData, Joiner.on("\n").join(customQueryAndTitleMappings)); return metaData; } - final Mapping mapping = datasets.get(0); + final CustomQueryAndTitleMapping customQueryAndTitleMapping = datasets.get(0); - mapMetaData(metaData, mapping); + mapMetaData(metaData, customQueryAndTitleMapping); return metaData; } @@ -154,20 +147,20 @@ public class CustomQueryAndTitleMapping { public TestResponse testMapping(@RequestBody TestRequest testRequest) { MetaData metaData = new MetaData(); final String exampleInput = testRequest.exampleInput; - if (!testRequest.mapping.getFromPattern().matcher(exampleInput).matches()) { + if (!testRequest.customQueryAndTitleMapping.getFromPattern().matcher(exampleInput).matches()) { return new TestResponse(null, null, false); } - if (testRequest.mapping.affectedValue == AffectedValue.QUERY) { + if (testRequest.customQueryAndTitleMapping.getAffectedValue() == AffectedValue.QUERY) { metaData.setQuery(exampleInput); } else { metaData.setTitle(exampleInput); } - metaData.setSearchType(testRequest.mapping.searchType); + metaData.setSearchType(testRequest.customQueryAndTitleMapping.getSearchType()); metaData.setSeason(1); metaData.setEpisode(2); try { - mapMetaData(metaData, testRequest.mapping); - if (testRequest.mapping.affectedValue == AffectedValue.QUERY) { + mapMetaData(metaData, testRequest.customQueryAndTitleMapping); + if (testRequest.customQueryAndTitleMapping.getAffectedValue() == AffectedValue.QUERY) { return new TestResponse(metaData.getQuery().get(), null, true); } else { return new TestResponse(metaData.getTitle().get(), null, true); @@ -179,27 +172,27 @@ public class CustomQueryAndTitleMapping { } - protected void mapMetaData(MetaData metaData, Mapping mapping) { + protected void mapMetaData(MetaData metaData, CustomQueryAndTitleMapping customQueryAndTitleMapping) { //What should happen: q=Boku no Hero Academia S4, season=4, ep=21 -> Boku no Hero Academia s04e21 //What the user should enter roughly: {0:(my hero academia|Boku no Hero Academia) {ignore:.*} -> {0} s{season:00} e{episode:00} //How it's configured: "TVSEARCH;QUERY;{0:(my hero academia|Boku no Hero Academia) {ignore:.*};{0} s{season:00} e{episode:00}" //{title:the haunting} {0:.*} -> The Haunting of Bly Manor {0} - if (mapping.affectedValue == AffectedValue.QUERY && metaData.getQuery().isPresent()) { - final String newQuery = mapValue(metaData, mapping, metaData.getQuery().get()); + if (customQueryAndTitleMapping.getAffectedValue() == AffectedValue.QUERY && metaData.getQuery().isPresent()) { + final String newQuery = mapValue(metaData, customQueryAndTitleMapping, metaData.getQuery().get()); metaData.setQuery(newQuery); - } else if ((mapping.affectedValue == AffectedValue.TITLE || mapping.affectedValue == AffectedValue.RESULT_TITLE) && metaData.getTitle().isPresent()) { - final String newTitle = mapValue(metaData, mapping, metaData.getTitle().get()); + } else if ((customQueryAndTitleMapping.getAffectedValue() == AffectedValue.TITLE || customQueryAndTitleMapping.getAffectedValue() == AffectedValue.RESULT_TITLE) && metaData.getTitle().isPresent()) { + final String newTitle = mapValue(metaData, customQueryAndTitleMapping, metaData.getTitle().get()); metaData.setTitle(newTitle); } } - private String mapValue(MetaData metaData, Mapping mapping, String value) { - logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Mapping input \"{}\" using dataset \"{}\"", value, mapping); + private String mapValue(MetaData metaData, CustomQueryAndTitleMapping customQueryAndTitleMapping, String value) { + logger.debug(LoggingMarkers.CUSTOM_MAPPING, "CustomQueryAndTitleMapping input \"{}\" using dataset \"{}\"", value, customQueryAndTitleMapping); String mappedValue = value; - String replacementRegex = mapping.to; + String replacementRegex = customQueryAndTitleMapping.getTo(); if (metaData.getSeason().isPresent()) { replacementRegex = replacementRegex.replace("{season:00}", String.format("%02d", metaData.getSeason().get())); replacementRegex = replacementRegex.replace("{season:0}", String.valueOf(metaData.getSeason().get())); @@ -214,77 +207,37 @@ public class CustomQueryAndTitleMapping { replacementRegex = replacementRegex.replace("{episode}", String.valueOf(metaData.getEpisode().get())); } replacementRegex = replacementRegex.replaceAll("\\{(?[^\\}]*)\\}", "\\$\\{hydra${groupName}\\}"); - logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Mapping input \"{}\" using replacement regex \"{}\"", value, replacementRegex); - mappedValue = mapping.getFromPattern().matcher(mappedValue).replaceFirst(replacementRegex); + logger.debug(LoggingMarkers.CUSTOM_MAPPING, "CustomQueryAndTitleMapping input \"{}\" using replacement regex \"{}\"", value, replacementRegex); + mappedValue = customQueryAndTitleMapping.getFromPattern().matcher(mappedValue).replaceFirst(replacementRegex); logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Mapped input \"{}\" to \"{}\"", value, mappedValue); return mappedValue; } - protected boolean isDatasetMatch(MetaData metaData, Mapping mapping) { - if (mapping.affectedValue == AffectedValue.QUERY && metaData.getQuery().isPresent()) { - final boolean matches = mapping.getFromPattern().matcher(metaData.getQuery().get()).matches(); - logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Query \"{}\" matches regex \"{}\": {}", metaData.getQuery().get(), mapping.getFromPattern().pattern(), matches); + protected boolean isDatasetMatch(MetaData metaData, CustomQueryAndTitleMapping customQueryAndTitleMapping) { + if (customQueryAndTitleMapping.getAffectedValue() == AffectedValue.QUERY && metaData.getQuery().isPresent()) { + final boolean matches = customQueryAndTitleMapping.getFromPattern().matcher(metaData.getQuery().get()).matches(); + logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Query \"{}\" matches regex \"{}\": {}", metaData.getQuery().get(), customQueryAndTitleMapping.getFromPattern().pattern(), matches); return matches; } - if ((mapping.affectedValue == AffectedValue.RESULT_TITLE || mapping.affectedValue == AffectedValue.TITLE) && metaData.getTitle().isPresent()) { - final boolean matches = mapping.getFromPattern().matcher(metaData.getTitle().get()).matches(); - logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Title \"{}\" matches regex \"{}\": {}", metaData.getTitle().get(), mapping.getFromPattern().pattern(), matches); + if ((customQueryAndTitleMapping.getAffectedValue() == AffectedValue.RESULT_TITLE || customQueryAndTitleMapping.getAffectedValue() == AffectedValue.TITLE) && metaData.getTitle().isPresent()) { + final boolean matches = customQueryAndTitleMapping.getFromPattern().matcher(metaData.getTitle().get()).matches(); + logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Title \"{}\" matches regex \"{}\": {}", metaData.getTitle().get(), customQueryAndTitleMapping.getFromPattern().pattern(), matches); return matches; } - logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Dataset does not match search request.\nDataset: {}\nSearch request:{}", mapping.from, metaData); + logger.debug(LoggingMarkers.CUSTOM_MAPPING, "Dataset does not match search request.\nDataset: {}\nSearch request:{}", customQueryAndTitleMapping.getFrom(), metaData); return false; } @Data - public static class Mapping { - - private SearchType searchType; - private AffectedValue affectedValue; - private String from; - private String to; - @JsonIgnore - private Pattern fromPattern; - - public Mapping() { - } - - public Mapping(String configValue) { - final String[] split = configValue.split(";"); - if (split.length != 4) { - throw new IllegalArgumentException("Unable to parse value: " + configValue); - } - this.searchType = split[0].equals("null") ? SearchType.SEARCH : SearchType.valueOf(split[0].toUpperCase()); - this.affectedValue = AffectedValue.valueOf(split[1].toUpperCase()); - this.from = split[2]; - this.to = split[3]; - } - - @JsonIgnore - public Pattern getFromPattern() { - if (fromPattern == null) { - String regex = from.replaceAll("\\{(?[^:]*):(?[^\\{\\}]*)\\}", "(?${hydraContent})"); - fromPattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); - } - return fromPattern; - } - - @Override - public String toString() { - return new StringJoiner(", ", Mapping.class.getSimpleName() + "[", "]") - .add("from='" + from + "'") - .add("to='" + to + "'") - .toString(); - } - } - - @Data +@ReflectionMarker static class TestRequest { - private Mapping mapping; + private CustomQueryAndTitleMapping customQueryAndTitleMapping; private String exampleInput; } @Data +@ReflectionMarker @AllArgsConstructor static class TestResponse { private final String output; @@ -293,6 +246,7 @@ public class CustomQueryAndTitleMapping { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor static class MetaData { diff --git a/core/src/main/java/org/nzbhydra/searching/DuplicateDetector.java b/core/src/main/java/org/nzbhydra/searching/DuplicateDetector.java index e3b355138..f4f2b9e1e 100644 --- a/core/src/main/java/org/nzbhydra/searching/DuplicateDetector.java +++ b/core/src/main/java/org/nzbhydra/searching/DuplicateDetector.java @@ -4,6 +4,7 @@ import com.google.common.collect.HashMultiset; import com.google.common.collect.Iterables; import com.google.common.collect.Multiset; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.indexers.Indexer; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.searching.dtoseventsenums.DuplicateDetectionResult; @@ -51,7 +52,7 @@ public class DuplicateDetector { boolean foundBucket = false; //Iterate over already existing buckets for (LinkedHashSet bucket : listOfBuckets) { - if (bucket.stream().map(SearchResultItem::getIndexer).collect(Collectors.toList()).contains(searchResultItem.getIndexer())) { + if (bucket.stream().map(SearchResultItem::getIndexer).toList().contains(searchResultItem.getIndexer())) { continue; } //And all searchResults in those buckets @@ -102,7 +103,7 @@ public class DuplicateDetector { return false; } - if (result1.getDownloadType() == SearchResultItem.DownloadType.TORRENT || result2.getDownloadType() == SearchResultItem.DownloadType.TORRENT) { + if (result1.getDownloadType() == DownloadType.TORRENT || result2.getDownloadType() == DownloadType.TORRENT) { logger.debug(LoggingMarkers.DUPLICATES, "Torrent download type(s). Type 1: {}. Type 2: {}", result1.getDownloadType(), result2.getDownloadType()); return false; } diff --git a/core/src/main/java/org/nzbhydra/searching/IndexerForSearchSelector.java b/core/src/main/java/org/nzbhydra/searching/IndexerForSearchSelector.java index 8a8bb67f5..176c678f7 100644 --- a/core/src/main/java/org/nzbhydra/searching/IndexerForSearchSelector.java +++ b/core/src/main/java/org/nzbhydra/searching/IndexerForSearchSelector.java @@ -4,25 +4,28 @@ import com.google.common.base.Joiner; import com.google.common.base.Stopwatch; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; -import lombok.AllArgsConstructor; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.category.Category.Subtype; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.Indexer; import org.nzbhydra.indexers.IndexerApiAccessType; import org.nzbhydra.indexers.status.IndexerLimit; import org.nzbhydra.indexers.status.IndexerLimitRepository; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.mediainfo.InfoProvider; -import org.nzbhydra.searching.dtoseventsenums.DownloadType; import org.nzbhydra.searching.dtoseventsenums.IndexerSelectionEvent; import org.nzbhydra.searching.dtoseventsenums.SearchMessageEvent; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -30,9 +33,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import org.springframework.web.context.annotation.RequestScope; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.Query; import java.sql.Timestamp; import java.time.Clock; import java.time.DayOfWeek; @@ -85,7 +85,7 @@ public class IndexerForSearchSelector { public IndexerForSearchSelection pickIndexers(SearchRequest searchRequest) { this.searchRequest = searchRequest; //Check any indexer that's not disabled by the user. If it's disabled by the system it will be deselected with a proper message later - List eligibleIndexers = searchModuleProvider.getIndexers().stream().filter(x -> x.getConfig().getState() != IndexerConfig.State.DISABLED_USER).collect(Collectors.toList()); + List eligibleIndexers = searchModuleProvider.getIndexers().stream().filter(x -> x.getConfig().getState() != IndexerConfig.State.DISABLED_USER).toList(); if (eligibleIndexers.isEmpty()) { logger.warn("You don't have any enabled indexers"); return new IndexerForSearchSelection(); @@ -159,11 +159,11 @@ public class IndexerForSearchSelector { } protected boolean checkSearchId(Indexer indexer) { - boolean needToSearchById = !searchRequest.getIdentifiers().isEmpty() && !searchRequest.getQuery().isPresent(); + boolean needToSearchById = !searchRequest.getIdentifiers().isEmpty() && searchRequest.getQuery().isEmpty(); if (needToSearchById) { boolean canUseAnyProvidedId = !Collections.disjoint(searchRequest.getIdentifiers().keySet(), indexer.getConfig().getSupportedSearchIds()); boolean cannotSearchProvidedOrConvertableId = !canUseAnyProvidedId && !infoProvider.canConvertAny(searchRequest.getIdentifiers().keySet(), Sets.newHashSet(indexer.getConfig().getSupportedSearchIds())); - boolean queryGenerationEnabled = configProvider.getBaseConfig().getSearching().getGenerateQueries().meets(searchRequest); + boolean queryGenerationEnabled = searchRequest.meets(configProvider.getBaseConfig().getSearching().getGenerateQueries()); if (cannotSearchProvidedOrConvertableId && !queryGenerationEnabled) { String message = String.format("Not using %s because the search did not provide any ID that the indexer can handle and query generation is disabled", indexer.getName()); return handleIndexerNotSelected(indexer, message, "No usable ID"); @@ -175,7 +175,7 @@ public class IndexerForSearchSelector { protected boolean checkSearchType(Indexer indexer) { boolean audioOrBookSearch = searchRequest.getSearchType() == SearchType.BOOK || searchRequest.getSearchType() == SearchType.MUSIC; if (audioOrBookSearch) { - boolean queryGenerationEnabled = configProvider.getBaseConfig().getSearching().getGenerateQueries().meets(searchRequest); + boolean queryGenerationEnabled = searchRequest.meets(configProvider.getBaseConfig().getSearching().getGenerateQueries()); boolean indexerSupportsType = indexer.getConfig().getSupportedSearchTypes().stream().anyMatch(x -> searchRequest.getSearchType().matches(x)); if (!indexerSupportsType && !queryGenerationEnabled) { String message = String.format("Not using %s because the search uses type %s which the indexer can't handle and query generation is disabled", searchRequest.getSearchType(), indexer.getName()); @@ -227,7 +227,7 @@ public class IndexerForSearchSelector { } protected boolean checkIndexerSelected(Indexer indexer) { - if (!searchRequest.getIndexers().isPresent()) { + if (searchRequest.getIndexers().isEmpty()) { return true; } if (searchRequest.getIndexers().get().isEmpty()) { @@ -258,7 +258,7 @@ public class IndexerForSearchSelector { protected boolean checkIndexerHitLimit(Indexer indexer) { Stopwatch stopwatch = Stopwatch.createStarted(); IndexerConfig indexerConfig = indexer.getConfig(); - if (!indexerConfig.getHitLimit().isPresent() && !indexerConfig.getDownloadLimit().isPresent()) { + if (indexerConfig.getHitLimit().isEmpty() && indexerConfig.getDownloadLimit().isEmpty()) { return true; } LocalDateTime comparisonTime; @@ -457,7 +457,7 @@ public class IndexerForSearchSelector { } protected boolean checkSearchSource(Indexer indexer) { - boolean wrongSearchSource = !indexer.getConfig().getEnabledForSearchSource().meets(searchRequest); + boolean wrongSearchSource = !searchRequest.meets(indexer.getConfig().getEnabledForSearchSource()); if (wrongSearchSource) { String message = String.format("Not using %s because the search source is %s but the indexer is only enabled for %s searches", indexer.getName(), searchRequest.getSource(), indexer.getConfig().getEnabledForSearchSource()); return handleIndexerNotSelected(indexer, message, "Not enabled for this search context"); @@ -552,11 +552,16 @@ public class IndexerForSearchSelector { @Data - @AllArgsConstructor +@ReflectionMarker @NoArgsConstructor public static class IndexerForSearchSelection { private Map notPickedIndexersWithReason = new HashMap<>(); private List selectedIndexers = new ArrayList<>(); + + public IndexerForSearchSelection(Map notPickedIndexersWithReason, List selectedIndexers) { + this.notPickedIndexersWithReason = notPickedIndexersWithReason; + this.selectedIndexers = selectedIndexers; + } } diff --git a/core/src/main/java/org/nzbhydra/searching/IndexerInstantiator.java b/core/src/main/java/org/nzbhydra/searching/IndexerInstantiator.java new file mode 100644 index 000000000..f9313e669 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/searching/IndexerInstantiator.java @@ -0,0 +1,102 @@ +/* + * (C) Copyright 2022 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.searching; + +import org.nzbhydra.config.BaseConfigHandler; +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.indexers.Anizb; +import org.nzbhydra.indexers.Binsearch; +import org.nzbhydra.indexers.DogNzb; +import org.nzbhydra.indexers.Indexer; +import org.nzbhydra.indexers.IndexerApiAccessEntityShortRepository; +import org.nzbhydra.indexers.IndexerApiAccessRepository; +import org.nzbhydra.indexers.IndexerRepository; +import org.nzbhydra.indexers.IndexerWebAccess; +import org.nzbhydra.indexers.Newznab; +import org.nzbhydra.indexers.NzbGeek; +import org.nzbhydra.indexers.NzbIndex; +import org.nzbhydra.indexers.QueryGenerator; +import org.nzbhydra.indexers.status.IndexerLimitRepository; +import org.nzbhydra.indexers.torznab.Torznab; +import org.nzbhydra.mediainfo.InfoProvider; +import org.nzbhydra.searching.db.SearchResultRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.oxm.Unmarshaller; +import org.springframework.stereotype.Component; + +@Component +public class IndexerInstantiator { + + @Autowired + protected ConfigProvider configProvider; + @Autowired + protected IndexerRepository indexerRepository; + @Autowired + protected SearchResultRepository searchResultRepository; + @Autowired + protected IndexerApiAccessRepository indexerApiAccessRepository; + @Autowired + protected IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository; + @Autowired + private IndexerLimitRepository indexerStatusRepository; + @Autowired + protected IndexerWebAccess indexerWebAccess; + @Autowired + protected SearchResultAcceptor resultAcceptor; + @Autowired + protected CategoryProvider categoryProvider; + @Autowired + protected InfoProvider infoProvider; + @Autowired + private ApplicationEventPublisher eventPublisher; + @Autowired + private QueryGenerator queryGenerator; + @Autowired + private CustomQueryAndTitleMappingHandler titleMapping; + @Autowired + private Unmarshaller unmarshaller; + @Autowired + private BaseConfigHandler baseConfigHandler; + + public Indexer instantiateIndexer(String name) { + switch (name) { + case "ANIZB" -> { + return new Anizb(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler); + } + case "BINSEARCH" -> { + return new Binsearch(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler); + } + case "DOGNZB" -> { + return new DogNzb(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler); + } + case "NEWZNAB" -> { + return new Newznab(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler); + } + case "NZBINDEX" -> { + return new NzbIndex(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler); + } + case "NZBGEEK" -> { + return new NzbGeek(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler); + } + case "TORZNAB" -> { + return new Torznab(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler); + } + } + throw new RuntimeException("Unable to instantiate " + name); + } +} diff --git a/core/src/main/java/org/nzbhydra/searching/IndexerSearchCacheEntry.java b/core/src/main/java/org/nzbhydra/searching/IndexerSearchCacheEntry.java index 5733d1881..bb82b59c8 100644 --- a/core/src/main/java/org/nzbhydra/searching/IndexerSearchCacheEntry.java +++ b/core/src/main/java/org/nzbhydra/searching/IndexerSearchCacheEntry.java @@ -22,6 +22,7 @@ import org.nzbhydra.indexers.Indexer; import org.nzbhydra.indexers.IndexerSearchEntity; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.ArrayList; import java.util.Collections; @@ -29,6 +30,7 @@ import java.util.Comparator; import java.util.List; @Data +@ReflectionMarker public class IndexerSearchCacheEntry { private Indexer indexer; diff --git a/core/src/main/java/org/nzbhydra/searching/SearchCacheEntry.java b/core/src/main/java/org/nzbhydra/searching/SearchCacheEntry.java index 8a19f9a42..18f2243e9 100644 --- a/core/src/main/java/org/nzbhydra/searching/SearchCacheEntry.java +++ b/core/src/main/java/org/nzbhydra/searching/SearchCacheEntry.java @@ -10,6 +10,7 @@ import org.nzbhydra.searching.IndexerForSearchSelector.IndexerForSearchSelection import org.nzbhydra.searching.db.SearchEntity; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.nzbhydra.springnative.ReflectionMarker; import java.time.Instant; import java.util.ArrayList; @@ -18,11 +19,12 @@ import java.util.List; import java.util.Map; @Data +@ReflectionMarker public class SearchCacheEntry { private Instant lastAccessed; private SearchRequest searchRequest; - private Map indexerCacheEntries = new HashMap<>(); + private Map indexerCacheEntries = new HashMap<>(); private List searchResultItems = new ArrayList<>(); private IndexerForSearchSelection indexerSelectionResult; private SearchEntity searchEntity; @@ -64,6 +66,11 @@ public class SearchCacheEntry { return numberOfAvailableResults = indexerCacheEntries.values().stream().mapToInt(x -> x.getSearchResultItems().size()).sum(); } + + public Map getIndexerCacheEntries() { + return indexerCacheEntries; + } + public boolean equals(Object obj) { if (obj == null) { return false; diff --git a/core/src/main/java/org/nzbhydra/searching/SearchModuleConfigProvider.java b/core/src/main/java/org/nzbhydra/searching/SearchModuleConfigProvider.java index 9d77732e0..0374fa1b5 100644 --- a/core/src/main/java/org/nzbhydra/searching/SearchModuleConfigProvider.java +++ b/core/src/main/java/org/nzbhydra/searching/SearchModuleConfigProvider.java @@ -1,7 +1,7 @@ package org.nzbhydra.searching; -import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigChangedEvent; +import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.indexer.IndexerConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,11 +25,10 @@ public class SearchModuleConfigProvider implements InitializingBean { @Autowired private SearchModuleProvider searchModuleProvider; @Autowired - private BaseConfig baseConfig; + private ConfigProvider configProvider; @EventListener public void handleNewConfig(ConfigChangedEvent configChangedEvent) { - baseConfig = configChangedEvent.getNewConfig(); afterPropertiesSet(); } @@ -44,7 +43,7 @@ public class SearchModuleConfigProvider implements InitializingBean { @Override public void afterPropertiesSet() { - indexers = baseConfig.getIndexers(); + indexers = configProvider.getBaseConfig().getIndexers(); searchModuleProvider.loadIndexers(indexers); } } diff --git a/core/src/main/java/org/nzbhydra/searching/SearchModuleProvider.java b/core/src/main/java/org/nzbhydra/searching/SearchModuleProvider.java index 38a14e009..64f4389e0 100644 --- a/core/src/main/java/org/nzbhydra/searching/SearchModuleProvider.java +++ b/core/src/main/java/org/nzbhydra/searching/SearchModuleProvider.java @@ -13,7 +13,6 @@ import org.nzbhydra.indexers.status.IndexerLimitRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -30,15 +29,14 @@ public class SearchModuleProvider { private static final Logger logger = LoggerFactory.getLogger(SearchModuleProvider.class); - @Autowired - private AutowireCapableBeanFactory beanFactory; - @Autowired private IndexerRepository indexerRepository; @Autowired private IndexerApiAccessEntityShortRepository shortRepository; @Autowired private IndexerLimitRepository indexerStatusRepository; + @Autowired + private IndexerInstantiator indexerInstantiator; private final Map searchModuleInstances = new HashMap<>(); private final Map apiHitsToStoreInitially = new HashMap<>(); @@ -47,6 +45,7 @@ public class SearchModuleProvider { private List indexerHandlingStrategies; + public List getIndexers() { return new ArrayList<>(searchModuleInstances.values()); } @@ -78,12 +77,12 @@ public class SearchModuleProvider { for (IndexerConfig config : indexers) { try { Optional optionalStrategy = indexerHandlingStrategies.stream().filter(x -> x.handlesIndexerConfig(config)).findFirst(); - if (!optionalStrategy.isPresent()) { + if (optionalStrategy.isEmpty()) { logger.error("Unable to find implementation for indexer type {} and host {}", config.getSearchModuleType(), config.getHost()); continue; } - Indexer searchModule = beanFactory.createBean(optionalStrategy.get().getIndexerClass()); + Indexer searchModule = indexerInstantiator.instantiateIndexer(optionalStrategy.get().getName()); logger.info("Initializing indexer {}", config.getName()); IndexerEntity indexerEntity = indexerRepository.findByName(config.getName()); @@ -113,7 +112,7 @@ public class SearchModuleProvider { } } logger.info("Finished initializing active indexers"); - List indexerNames = indexers.stream().map(IndexerConfig::getName).collect(Collectors.toList()); + List indexerNames = indexers.stream().map(IndexerConfig::getName).toList(); if (searchModuleInstances.isEmpty()) { logger.warn("No indexers configured"); diff --git a/core/src/main/java/org/nzbhydra/searching/SearchResultAcceptor.java b/core/src/main/java/org/nzbhydra/searching/SearchResultAcceptor.java index e1bfa22be..97898a426 100644 --- a/core/src/main/java/org/nzbhydra/searching/SearchResultAcceptor.java +++ b/core/src/main/java/org/nzbhydra/searching/SearchResultAcceptor.java @@ -6,27 +6,28 @@ import com.google.common.base.Strings; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import com.google.common.collect.Multiset.Entry; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.SearchSourceRestriction; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.config.indexer.SearchModuleType; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -39,7 +40,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; @Component public class SearchResultAcceptor { @@ -159,9 +159,9 @@ public class SearchResultAcceptor { Set messages = new HashSet<>(constraintViolations.size()); messages.addAll(constraintViolations.stream() - .map(constraintViolation -> String.format("%s value '%s' %s", constraintViolation.getPropertyPath(), - constraintViolation.getInvalidValue(), constraintViolation.getMessage())) - .collect(Collectors.toList())); + .map(constraintViolation -> String.format("%s value '%s' %s", constraintViolation.getPropertyPath(), + constraintViolation.getInvalidValue(), constraintViolation.getMessage())) + .toList()); logger.error("Coding error: SearchResultItem validation messages: {}", Joiner.on(" ").join(messages)); reasonsForRejection.add("Important data could not be mapped from the indexers returned response"); return false; @@ -179,7 +179,7 @@ public class SearchResultAcceptor { } protected boolean checkForCategoryShouldBeIgnored(SearchRequest searchRequest, Multiset reasonsForRejection, SearchResultItem item) { - if (item.getCategory().getIgnoreResultsFrom().meets(searchRequest)) { + if (searchRequest.meets(item.getCategory().getIgnoreResultsFrom())) { logger.debug(LoggingMarkers.RESULT_ACCEPTOR, "{} is in forbidden category {}", item.getTitle(), item.getCategory().getName()); reasonsForRejection.add("In forbidden category"); return false; @@ -397,6 +397,7 @@ public class SearchResultAcceptor { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class AcceptorResult { diff --git a/core/src/main/java/org/nzbhydra/searching/SearchState.java b/core/src/main/java/org/nzbhydra/searching/SearchState.java index 1903303da..9a426baf2 100644 --- a/core/src/main/java/org/nzbhydra/searching/SearchState.java +++ b/core/src/main/java/org/nzbhydra/searching/SearchState.java @@ -17,11 +17,13 @@ package org.nzbhydra.searching; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.ArrayList; import java.util.List; @Data +@ReflectionMarker class SearchState { private long searchRequestId; diff --git a/core/src/main/java/org/nzbhydra/searching/SearchWeb.java b/core/src/main/java/org/nzbhydra/searching/SearchWeb.java index b1067c652..f0a628053 100644 --- a/core/src/main/java/org/nzbhydra/searching/SearchWeb.java +++ b/core/src/main/java/org/nzbhydra/searching/SearchWeb.java @@ -5,16 +5,16 @@ import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.category.Category; -import org.nzbhydra.mediainfo.MediaIdType; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.searching.dtoseventsenums.FallbackSearchInitiatedEvent; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchFinishedEvent; import org.nzbhydra.searching.dtoseventsenums.IndexerSelectionEvent; import org.nzbhydra.searching.dtoseventsenums.SearchMessageEvent; import org.nzbhydra.searching.dtoseventsenums.SearchRequestParameters; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.nzbhydra.searching.searchrequests.SearchRequestFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +51,7 @@ public class SearchWeb { @Autowired private SimpMessageSendingOperations messagingTemplate; @Autowired - private CustomQueryAndTitleMapping customQueryAndTitleMapping; + private CustomQueryAndTitleMappingHandler customQueryAndTitleMappingHandler; private final Lock lock = new ReentrantLock(); @@ -143,7 +143,7 @@ public class SearchWeb { } searchRequest = searchRequestFactory.extendWithSavedIdentifiers(searchRequest); - searchRequest = customQueryAndTitleMapping.mapSearchRequest(searchRequest); + searchRequest = customQueryAndTitleMappingHandler.mapSearchRequest(searchRequest); //Initialize messages for this search request final SearchState searchState = new SearchState(searchRequest.getSearchRequestId()); diff --git a/core/src/main/java/org/nzbhydra/searching/Searcher.java b/core/src/main/java/org/nzbhydra/searching/Searcher.java index 22d3d27a4..1433abd7d 100644 --- a/core/src/main/java/org/nzbhydra/searching/Searcher.java +++ b/core/src/main/java/org/nzbhydra/searching/Searcher.java @@ -3,11 +3,12 @@ package org.nzbhydra.searching; import com.google.common.base.Stopwatch; import com.google.common.collect.Iterables; import com.google.common.collect.Multiset; +import jakarta.annotation.PreDestroy; import lombok.Getter; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; -import org.nzbhydra.ShutdownEvent; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.indexers.Indexer; import org.nzbhydra.indexers.IndexerSearchEntity; import org.nzbhydra.indexers.IndexerSearchRepository; @@ -23,13 +24,12 @@ import org.nzbhydra.searching.dtoseventsenums.DuplicateDetectionResult; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.ArrayList; @@ -83,6 +83,7 @@ public class Searcher { .expirationListener((k, v) -> logger.debug("Removing expired search cache entry {}", ((SearchCacheEntry) v).getSearchRequest())) .build(); + @Transactional public SearchResult search(SearchRequest searchRequest) { Stopwatch stopwatch = Stopwatch.createStarted(); eventPublisher.publishEvent(new SearchEvent(searchRequest)); @@ -118,10 +119,10 @@ public class Searcher { indexersWithCachedResults = getIndexersWithCachedResults(searchCacheEntry); while (!indexersWithCachedResults.isEmpty()) { - List newestItemsFromIndexers = indexersWithCachedResults.stream().map(IndexerSearchCacheEntry::peek).sorted(Comparator.comparingLong(x -> ((SearchResultItem) x).getBestDate().getEpochSecond()).reversed()).collect(Collectors.toList()); + List newestItemsFromIndexers = indexersWithCachedResults.stream().map(IndexerSearchCacheEntry::peek).sorted(Comparator.comparingLong(x -> ((SearchResultItem) x).getBestDate().getEpochSecond()).reversed()).toList(); SearchResultItem newestResult = newestItemsFromIndexers.get(0); Indexer newestResultIndexer = newestResult.getIndexer(); - IndexerSearchCacheEntry newestIndexerSearchCacheEntry = searchCacheEntry.getIndexerCacheEntries().get(newestResultIndexer); + IndexerSearchCacheEntry newestIndexerSearchCacheEntry = searchCacheEntry.getIndexerCacheEntries().get(newestResultIndexer.getName()); searchResultItems.add(newestIndexerSearchCacheEntry.pop()); indexersWithCachedResults = getIndexersWithCachedResults(searchCacheEntry); @@ -164,9 +165,9 @@ public class Searcher { } searchResult.setNumberOfTotalAvailableResults(searchCacheEntry.getNumberOfTotalAvailableResults()); searchResult.setIndexerSearchResults(searchCacheEntry.getIndexerCacheEntries().values().stream() - .filter(x -> !x.getIndexerSearchResults().isEmpty()) - .map(x -> Iterables.getLast(x.getIndexerSearchResults())) - .collect(Collectors.toList())); + .filter(x -> !x.getIndexerSearchResults().isEmpty()) + .map(x -> Iterables.getLast(x.getIndexerSearchResults())) + .collect(Collectors.toList())); searchResult.setReasonsForRejection(searchCacheEntry.getReasonsForRejection()); searchCacheEntry.setNumberOfRemovedDuplicates(searchResult.getNumberOfRemovedDuplicates()); @@ -265,11 +266,11 @@ public class Searcher { if (configProvider.getBaseConfig().getMain().isKeepHistory()) { entity = indexerSearchRepository.save(entity); for (SearchResultEntity x : indexerSearchResult.getSearchResultEntities()) { - x.setIndexerSearchEntity(entity); + x.setIndexerSearchEntityId(entity.getId()); } } searchResultRepository.saveAll(indexerSearchResult.getSearchResultEntities()); - searchCacheEntry.getIndexerCacheEntries().get(indexerSearchResult.getIndexer()).setIndexerSearchEntity(entity); + searchCacheEntry.getIndexerCacheEntries().get(indexerSearchResult.getIndexer().getName()).setIndexerSearchEntity(entity); countEntities++; } } @@ -313,7 +314,7 @@ public class Searcher { protected List getIndexersToSearch(SearchCacheEntry searchCacheEntry) { List indexerSearchCacheEntries = new ArrayList<>(); for (Indexer selectedIndexer : searchCacheEntry.getIndexerSelectionResult().getSelectedIndexers()) { - searchCacheEntry.getIndexerCacheEntries().putIfAbsent(selectedIndexer, new IndexerSearchCacheEntry(selectedIndexer)); + searchCacheEntry.getIndexerCacheEntries().putIfAbsent(selectedIndexer.getName(), new IndexerSearchCacheEntry(selectedIndexer)); } for (IndexerSearchCacheEntry indexerSearchCacheEntry : searchCacheEntry.getIndexerCacheEntries().values()) { @@ -361,8 +362,8 @@ public class Searcher { for (Future future : futures) { try { IndexerSearchResult indexerSearchResult = future.get(); - searchCacheEntry.getIndexerCacheEntries().get(indexerSearchResult.getIndexer()).addIndexerSearchResult(indexerSearchResult); - indexerSearchResults.put(indexerSearchResult.getIndexer(), searchCacheEntry.getIndexerCacheEntries().get(indexerSearchResult.getIndexer()).getIndexerSearchResults()); + searchCacheEntry.getIndexerCacheEntries().get(indexerSearchResult.getIndexer().getName()).addIndexerSearchResult(indexerSearchResult); + indexerSearchResults.put(indexerSearchResult.getIndexer(), searchCacheEntry.getIndexerCacheEntries().get(indexerSearchResult.getIndexer().getName()).getIndexerSearchResults()); } catch (ExecutionException e) { logger.error("Unexpected error while searching", e); } @@ -415,8 +416,8 @@ public class Searcher { } @SuppressWarnings("unused") - @EventListener - public void onShutdown(ShutdownEvent event) { + @PreDestroy + public void onShutdown() { shutdownRequested = true; synchronized (executors) { if (executors.size() > 0) { diff --git a/core/src/main/java/org/nzbhydra/searching/SortableMessage.java b/core/src/main/java/org/nzbhydra/searching/SortableMessage.java index 25f31aaba..8917f2f03 100644 --- a/core/src/main/java/org/nzbhydra/searching/SortableMessage.java +++ b/core/src/main/java/org/nzbhydra/searching/SortableMessage.java @@ -18,8 +18,10 @@ package org.nzbhydra.searching; import lombok.AllArgsConstructor; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor public class SortableMessage { private String message; diff --git a/core/src/main/java/org/nzbhydra/searching/cleanup/HistoryCleanupTask.java b/core/src/main/java/org/nzbhydra/searching/cleanup/HistoryCleanupTask.java index 9d64800fc..5b8f87e5c 100644 --- a/core/src/main/java/org/nzbhydra/searching/cleanup/HistoryCleanupTask.java +++ b/core/src/main/java/org/nzbhydra/searching/cleanup/HistoryCleanupTask.java @@ -139,7 +139,7 @@ public class HistoryCleanupTask { public void deleteOldIndexerApiAccesses(Instant deleteOlderThan, Connection connection) { logger.debug(LoggingMarkers.HISTORY_CLEANUP, "Deleting old indexer API accesses"); Optional optionalId = getIdBefore(deleteOlderThan, "INDEXERAPIACCESS", ASC_DESC.DESC, connection); - if (!optionalId.isPresent()) { + if (optionalId.isEmpty()) { logger.debug(LoggingMarkers.HISTORY_CLEANUP, "No older indexer API accesses to delete"); return; } diff --git a/core/src/main/java/org/nzbhydra/searching/cleanup/OldResultsCleanupTask.java b/core/src/main/java/org/nzbhydra/searching/cleanup/OldResultsCleanupTask.java index 06d0c5651..dd50fae78 100644 --- a/core/src/main/java/org/nzbhydra/searching/cleanup/OldResultsCleanupTask.java +++ b/core/src/main/java/org/nzbhydra/searching/cleanup/OldResultsCleanupTask.java @@ -17,6 +17,7 @@ package org.nzbhydra.searching.cleanup; import com.google.common.base.Stopwatch; +import jakarta.persistence.EntityManager; import org.nzbhydra.NzbHydra; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.logging.LoggingMarkers; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import javax.persistence.EntityManager; import java.io.File; import java.time.Instant; import java.time.temporal.ChronoUnit; diff --git a/core/src/main/java/org/nzbhydra/searching/db/IdentifierKeyValuePair.java b/core/src/main/java/org/nzbhydra/searching/db/IdentifierKeyValuePair.java index 03201af09..6e1994711 100644 --- a/core/src/main/java/org/nzbhydra/searching/db/IdentifierKeyValuePair.java +++ b/core/src/main/java/org/nzbhydra/searching/db/IdentifierKeyValuePair.java @@ -1,5 +1,5 @@ /* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,28 @@ package org.nzbhydra.searching.db; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; import java.util.Objects; @Data +@ReflectionMarker @Entity @NoArgsConstructor -public class IdentifierKeyValuePair { +public final class IdentifierKeyValuePair { @Id @GeneratedValue + @JsonIgnore + @SequenceGenerator(allocationSize = 1, name = "IDENTIFIER_KEY_VALUE_PAIR_SEQ") private Integer id; public IdentifierKeyValuePair(String identifierKey, String identifierValue) { diff --git a/core/src/main/java/org/nzbhydra/searching/db/SearchEntity.java b/core/src/main/java/org/nzbhydra/searching/db/SearchEntity.java index 44adbc54e..30a23ae21 100644 --- a/core/src/main/java/org/nzbhydra/searching/db/SearchEntity.java +++ b/core/src/main/java/org/nzbhydra/searching/db/SearchEntity.java @@ -1,5 +1,5 @@ /* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,26 @@ package org.nzbhydra.searching.db; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; -import org.nzbhydra.searching.dtoseventsenums.SearchType; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.searching.SearchType; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.web.SessionStorage; -import javax.persistence.CascadeType; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.OneToMany; -import javax.persistence.Table; import java.time.Instant; import java.util.HashSet; import java.util.Objects; @@ -40,12 +43,14 @@ import java.util.Set; @Data +@ReflectionMarker @Entity @Table(name = "search") -public class SearchEntity { +public final class SearchEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @SequenceGenerator(allocationSize = 1, name = "SEARCH_SEQ") private int id; @Enumerated(EnumType.STRING) @@ -77,7 +82,7 @@ public class SearchEntity { this.ip = SessionStorage.IP.get(); } - + @JsonIgnore public boolean equalsSearchEntity(SearchEntity that) { return Objects.equals(categoryName, that.categoryName) && Objects.equals(query, that.query) && @@ -88,6 +93,7 @@ public class SearchEntity { Objects.equals(author, that.author); } + @JsonIgnore public int getComparingHash() { return Objects.hash(getQuery(), getCategoryName(), getSeason(), getEpisode(), getTitle(), identifiers); } diff --git a/core/src/main/java/org/nzbhydra/searching/db/SearchResultEntity.java b/core/src/main/java/org/nzbhydra/searching/db/SearchResultEntity.java index f0aaefcd5..7e7e0863d 100644 --- a/core/src/main/java/org/nzbhydra/searching/db/SearchResultEntity.java +++ b/core/src/main/java/org/nzbhydra/searching/db/SearchResultEntity.java @@ -16,87 +16,81 @@ package org.nzbhydra.searching.db; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.indexers.IndexerEntity; -import org.nzbhydra.indexers.IndexerSearchEntity; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; -import javax.persistence.Column; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import javax.validation.constraints.NotNull; import java.time.Instant; @Entity @Getter @Table(name = "searchresult" - , indexes = { - @Index(columnList = "indexer_id,indexerguid", unique = true)} + , indexes = { + @Index(columnList = "indexer_id,indexerguid", unique = true)} ) -public class SearchResultEntity { +public final class SearchResultEntity { @GenericGenerator( - name = "search-result-sequence", - strategy = "org.nzbhydra.searching.db.SearchResultSequenceGenerator", - parameters = @org.hibernate.annotations.Parameter( - name = "sequence_name", - value = "hibernate_sequence" - ) + name = "search-result-sequence", + strategy = "org.nzbhydra.searching.db.SearchResultSequenceGenerator", + parameters = {@org.hibernate.annotations.Parameter( + name = "sequence_name", + value = "HIBERNATE_SEQUENCE" + ), + @org.hibernate.annotations.Parameter( + name = "increment_size", + value = "1" + )} ) @Id @GeneratedValue(generator = "search-result-sequence", strategy = GenerationType.SEQUENCE) - @JsonSerialize(using = ToStringSerializer.class) //JS cannot handle long. We don't need to calculate with this so string is fine to not lose any digits - protected long id; + private long id; @ManyToOne @NotNull @OnDelete(action = OnDeleteAction.CASCADE) - protected IndexerEntity indexer; + private IndexerEntity indexer; @Convert(converter = org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters.InstantConverter.class) - protected Instant firstFound; + private Instant firstFound; @NotNull @Column(length = 4000) - protected String title; + private String title; @Column(name = "indexerguid") @NotNull - protected String indexerGuid; + private String indexerGuid; @Column(length = 4000) - protected String link; + private String link; @Column(length = 4000) - protected String details; + private String details; @Enumerated(EnumType.STRING) - protected DownloadType downloadType; + private DownloadType downloadType; @Convert(converter = org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters.InstantConverter.class) - protected Instant pubDate; + private Instant pubDate; - @JsonIgnore - @OneToOne(fetch = FetchType.LAZY) //Only needed for the uniqueness saver, may be deleted later and then not needed anymore - @JoinColumn(name = "indexersearchentity") - private IndexerSearchEntity indexerSearchEntity; + @Column(name = "INDEXERSEARCHENTITY") + private Integer indexerSearchEntityId; public SearchResultEntity() { } @@ -148,12 +142,12 @@ public class SearchResultEntity { this.pubDate = pubDate; } - public IndexerSearchEntity getIndexerSearchEntity() { - return indexerSearchEntity; + public Integer getIndexerSearchEntityId() { + return indexerSearchEntityId; } - public void setIndexerSearchEntity(IndexerSearchEntity indexerSearchEntity) { - this.indexerSearchEntity = indexerSearchEntity; + public void setIndexerSearchEntityId(Integer indexerSearchEntityId) { + this.indexerSearchEntityId = indexerSearchEntityId; } @Override diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/DuplicateDetectionResult.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/DuplicateDetectionResult.java index 694262e59..2a093bdaf 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/DuplicateDetectionResult.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/DuplicateDetectionResult.java @@ -21,11 +21,13 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.indexers.Indexer; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.LinkedHashSet; import java.util.List; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class DuplicateDetectionResult { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/FallbackSearchInitiatedEvent.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/FallbackSearchInitiatedEvent.java index cc74a87a4..8068925b4 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/FallbackSearchInitiatedEvent.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/FallbackSearchInitiatedEvent.java @@ -20,8 +20,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class FallbackSearchInitiatedEvent { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchFinishedEvent.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchFinishedEvent.java index 444c059aa..ef33813b8 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchFinishedEvent.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchFinishedEvent.java @@ -20,8 +20,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class IndexerSearchFinishedEvent { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchResult.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchResult.java index 5ed2c80e8..68282fb06 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchResult.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchResult.java @@ -23,6 +23,7 @@ import com.google.common.collect.Multiset; import lombok.Data; import org.nzbhydra.indexers.Indexer; import org.nzbhydra.searching.db.SearchResultEntity; +import org.nzbhydra.springnative.ReflectionMarker; import java.time.Instant; import java.util.ArrayList; @@ -33,6 +34,7 @@ import java.util.Set; import java.util.stream.Collectors; @Data +@ReflectionMarker public class IndexerSearchResult { private Indexer indexer; diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSelectionEvent.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSelectionEvent.java index c51569f50..fcd251e58 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSelectionEvent.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSelectionEvent.java @@ -20,8 +20,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class IndexerSelectionEvent { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/OffsetAndLimitCalculation.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/OffsetAndLimitCalculation.java index fcbfd7cd9..d62a94722 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/OffsetAndLimitCalculation.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/OffsetAndLimitCalculation.java @@ -19,8 +19,10 @@ package org.nzbhydra.searching.dtoseventsenums; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class OffsetAndLimitCalculation { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/RejectionReason.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/RejectionReason.java index e011a36c2..59e607931 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/RejectionReason.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/RejectionReason.java @@ -19,8 +19,10 @@ package org.nzbhydra.searching.dtoseventsenums; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class RejectionReason { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchMessageEvent.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchMessageEvent.java index d21de99d3..5488980ab 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchMessageEvent.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchMessageEvent.java @@ -21,8 +21,10 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.nzbhydra.searching.SortableMessage; import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public class SearchMessageEvent { diff --git a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultItem.java b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultItem.java index 48ca4dfb4..fa52d2ca7 100644 --- a/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultItem.java +++ b/core/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultItem.java @@ -18,12 +18,14 @@ package org.nzbhydra.searching.dtoseventsenums; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.nzbhydra.config.category.Category; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.indexers.Indexer; +import org.nzbhydra.springnative.ReflectionMarker; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Comparator; @@ -32,6 +34,7 @@ import java.util.Map; import java.util.Optional; @Data +@ReflectionMarker public class SearchResultItem { public enum HasNfo { @@ -40,10 +43,6 @@ public class SearchResultItem { MAYBE } - public enum DownloadType { - NZB, - TORRENT - } //Note: Validation annotations relate to the needed state after the item was created by an indexer private boolean agePrecise; private Map attributes = new HashMap<>(); @@ -134,10 +133,9 @@ public class SearchResultItem { if (this == o) { return true; } - if (!(o instanceof SearchResultItem)) { + if (!(o instanceof SearchResultItem item)) { return false; } - SearchResultItem item = (SearchResultItem) o; return Objects.equal(indexer, item.indexer) && Objects.equal(indexerGuid, item.indexerGuid) && Objects.equal(link, item.link) && diff --git a/core/src/main/java/org/nzbhydra/searching/searchrequests/InternalData.java b/core/src/main/java/org/nzbhydra/searching/searchrequests/InternalData.java index e0cd3e482..d47fd82ef 100644 --- a/core/src/main/java/org/nzbhydra/searching/searchrequests/InternalData.java +++ b/core/src/main/java/org/nzbhydra/searching/searchrequests/InternalData.java @@ -1,6 +1,7 @@ package org.nzbhydra.searching.searchrequests; import lombok.Data; +import org.nzbhydra.springnative.ReflectionMarker; import java.util.ArrayList; import java.util.HashMap; @@ -9,6 +10,7 @@ import java.util.Map; import java.util.Optional; @Data +@ReflectionMarker public class InternalData { public enum FallbackState { diff --git a/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequest.java b/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequest.java index 7e0d625e1..fefa20ae1 100644 --- a/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequest.java +++ b/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequest.java @@ -5,10 +5,13 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import lombok.Data; import lombok.NoArgsConstructor; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.SearchSourceRestriction; import org.nzbhydra.config.category.Category; -import org.nzbhydra.mediainfo.MediaIdType; -import org.nzbhydra.searching.dtoseventsenums.DownloadType; -import org.nzbhydra.searching.dtoseventsenums.SearchType; +import org.nzbhydra.config.downloading.DownloadType; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,14 +25,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; @Data +@ReflectionMarker @NoArgsConstructor public class SearchRequest { - public enum SearchSource { - INTERNAL, - API - } - private static final Logger logger = LoggerFactory.getLogger(SearchRequest.class); private static final Pattern EXCLUSION_PATTERN = Pattern.compile("[\\s|\b](\\-\\-|!)(?\\w+)"); @@ -138,15 +137,25 @@ public class SearchRequest { return this; } + public boolean meets(SearchSourceRestriction restriction) { + if (restriction == SearchSourceRestriction.ALL_BUT_RSS && getSource() == SearchSource.API) { + return getQuery().isPresent() || !getIdentifiers().isEmpty(); + } + if (restriction == SearchSourceRestriction.ONLY_RSS && getSource() == SearchSource.API) { + return getQuery().isEmpty() && getIdentifiers().isEmpty(); + } + return getSource().meets(restriction); + } + @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("source", source) - .add("indexers", indexers) - .add("searchType", searchType) - .add("category", category.getName()) - .add("offset", offset) - .add("limit", limit) + .add("source", source) + .add("indexers", indexers) + .add("searchType", searchType) + .add("category", category.getName()) + .add("offset", offset) + .add("limit", limit) .add("minsize", minsize) .add("maxsize", maxsize) .add("minage", minage) diff --git a/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequestFactory.java b/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequestFactory.java index 932e1e573..041f065aa 100644 --- a/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequestFactory.java +++ b/core/src/main/java/org/nzbhydra/searching/searchrequests/SearchRequestFactory.java @@ -1,15 +1,15 @@ package org.nzbhydra.searching.searchrequests; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.SearchingConfig; import org.nzbhydra.config.category.Category; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.mediainfo.Imdb; import org.nzbhydra.mediainfo.InfoProvider; -import org.nzbhydra.mediainfo.MediaIdType; import org.nzbhydra.mediainfo.MovieInfo; import org.nzbhydra.mediainfo.TvInfo; -import org.nzbhydra.searching.dtoseventsenums.SearchType; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -36,7 +36,7 @@ public class SearchRequestFactory { searchRequest.setCategory(category); searchRequest.setSearchRequestId(searchRequestId); MDC.put("SEARCH", String.valueOf(searchRequestId)); - if (!searchRequest.getMaxage().isPresent() && searchingConfig.getMaxAge().isPresent()) { + if (searchRequest.getMaxage().isEmpty() && searchingConfig.getMaxAge().isPresent()) { searchRequest.setMaxage(searchingConfig.getMaxAge().get()); } diff --git a/core/src/main/java/org/nzbhydra/searching/uniqueness/IndexerUniquenessScoreEntity.java b/core/src/main/java/org/nzbhydra/searching/uniqueness/IndexerUniquenessScoreEntity.java index cf2532ded..bca687949 100644 --- a/core/src/main/java/org/nzbhydra/searching/uniqueness/IndexerUniquenessScoreEntity.java +++ b/core/src/main/java/org/nzbhydra/searching/uniqueness/IndexerUniquenessScoreEntity.java @@ -16,27 +16,30 @@ package org.nzbhydra.searching.uniqueness; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Data; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.nzbhydra.indexers.IndexerEntity; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.Table; +import org.nzbhydra.springnative.ReflectionMarker; @Entity @Data +@ReflectionMarker @Table(name = "indexeruniquenessscore") -public class IndexerUniquenessScoreEntity { +public final class IndexerUniquenessScoreEntity { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) + @SequenceGenerator(allocationSize = 1, name = "INDEXERUNIQUENESSSCORE_SEQ") private int id; @ManyToOne diff --git a/core/src/main/java/org/nzbhydra/systemcontrol/SystemControl.java b/core/src/main/java/org/nzbhydra/systemcontrol/SystemControl.java index 41e48c3f1..7d5879783 100644 --- a/core/src/main/java/org/nzbhydra/systemcontrol/SystemControl.java +++ b/core/src/main/java/org/nzbhydra/systemcontrol/SystemControl.java @@ -18,7 +18,6 @@ package org.nzbhydra.systemcontrol; import org.nzbhydra.NzbHydra; import org.nzbhydra.ShutdownEvent; -import org.nzbhydra.WindowsTrayIcon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -65,9 +64,6 @@ public class SystemControl { Thread.sleep(300); applicationEventPublisher.publishEvent(new ShutdownEvent()); ((ConfigurableApplicationContext) NzbHydra.getApplicationContext()).close(); - if (NzbHydra.isOsWindows()) { - WindowsTrayIcon.remove(); - } System.exit(returnCode); } catch (InterruptedException e) { logger.error("Error while waiting to exit", e); //Doesn't ever happen anyway diff --git a/core/src/main/java/org/nzbhydra/systemcontrol/SystemControlWeb.java b/core/src/main/java/org/nzbhydra/systemcontrol/SystemControlWeb.java index 717d7ac6d..7bdf77df3 100644 --- a/core/src/main/java/org/nzbhydra/systemcontrol/SystemControlWeb.java +++ b/core/src/main/java/org/nzbhydra/systemcontrol/SystemControlWeb.java @@ -16,6 +16,7 @@ package org.nzbhydra.systemcontrol; +import org.jetbrains.annotations.NotNull; import org.nzbhydra.GenericResponse; import org.nzbhydra.web.UrlCalculator; import org.slf4j.Logger; @@ -40,6 +41,11 @@ public class SystemControlWeb { @Secured({"ROLE_ADMIN"}) @RequestMapping(value = "/internalapi/control/shutdown", method = RequestMethod.GET) public GenericResponse shutdown() throws Exception { + return doShutdown(); + } + + @NotNull + private GenericResponse doShutdown() { logger.info("Shutting down due to external request"); systemControl.exitWithReturnCode(SystemControl.SHUTDOWN_RETURN_CODE); return GenericResponse.ok(); @@ -49,6 +55,11 @@ public class SystemControlWeb { @Secured({"ROLE_ADMIN"}) @RequestMapping(value = "/internalapi/control/restart", method = RequestMethod.GET) public GenericResponse restart() throws Exception { + return doRestart(); + } + + @NotNull + private GenericResponse doRestart() { String baseUrl = urlCalculator.getRequestBasedUriBuilder().toUriString(); logger.info("Shutting down due to external request. Restart will be handled by wrapper. Web interface will reload to URL {}", baseUrl); systemControl.exitWithReturnCode(SystemControl.RESTART_RETURN_CODE); diff --git a/core/src/main/java/org/nzbhydra/tasks/HydraTaskScheduler.java b/core/src/main/java/org/nzbhydra/tasks/HydraTaskScheduler.java index d2b151581..9e5b80a04 100644 --- a/core/src/main/java/org/nzbhydra/tasks/HydraTaskScheduler.java +++ b/core/src/main/java/org/nzbhydra/tasks/HydraTaskScheduler.java @@ -17,11 +17,12 @@ package org.nzbhydra.tasks; import com.google.common.reflect.Invokable; +import jakarta.annotation.PreDestroy; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang3.time.DurationFormatUtils; -import org.nzbhydra.ShutdownEvent; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.support.AopUtils; @@ -29,7 +30,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.event.EventListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; @@ -40,10 +40,7 @@ import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.Instant; import java.util.ArrayList; -import java.util.Calendar; import java.util.Comparator; -import java.util.Date; -import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -66,10 +63,11 @@ public class HydraTaskScheduler implements BeanPostProcessor, SmartInitializingS private final Map taskSchedules = new HashMap<>(); private boolean shutdownRequested; - @EventListener - public void onShutdown(ShutdownEvent event) { + @PreDestroy + public void onShutdown() { taskSchedules.values().forEach(x -> x.cancel(false)); shutdownRequested = true; + scheduler.shutdown(); } @Override @@ -80,7 +78,7 @@ public class HydraTaskScheduler implements BeanPostProcessor, SmartInitializingS private void scheduleTasks() { for (TaskRuntimeInformation runtimeInformation : runtimeInformationMap.values()) { - scheduleTask(runtimeInformation, false); + scheduleTask(runtimeInformation, true); } } @@ -102,13 +100,12 @@ public class HydraTaskScheduler implements BeanPostProcessor, SmartInitializingS } ScheduledFuture scheduledTask = scheduler.schedule(runnable, new Trigger() { @Override - public Date nextExecutionTime(TriggerContext triggerContext) { - Calendar nextExecutionTime = new GregorianCalendar(); - Date lastCompletionTime = runNow ? new Date() : triggerContext.lastCompletionTime(); - nextExecutionTime.setTime(lastCompletionTime != null ? lastCompletionTime : new Date()); - nextExecutionTime.add(Calendar.MILLISECOND, (int) getIntervalForTask(task)); - taskInformations.put(task, new TaskInformation(task.name(), lastCompletionTime != null ? lastCompletionTime.toInstant() : null, nextExecutionTime.toInstant())); - return nextExecutionTime.getTime(); + public Instant nextExecution(TriggerContext triggerContext) { + Instant lastCompletionTime = runNow ? Instant.now() : triggerContext.lastCompletion(); + Instant nextExecutionTime = lastCompletionTime != null ? lastCompletionTime : Instant.now(); + nextExecutionTime = nextExecutionTime.plusMillis((int) getIntervalForTask(task)); + taskInformations.put(task, new TaskInformation(task.name(), lastCompletionTime, nextExecutionTime)); + return nextExecutionTime; } }); taskSchedules.put(task.name(), scheduledTask); @@ -155,6 +152,7 @@ public class HydraTaskScheduler implements BeanPostProcessor, SmartInitializingS } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class TaskInformation { @@ -164,6 +162,7 @@ public class HydraTaskScheduler implements BeanPostProcessor, SmartInitializingS } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class TaskRuntimeInformation { diff --git a/core/src/main/java/org/nzbhydra/update/UpdateData.java b/core/src/main/java/org/nzbhydra/update/UpdateData.java index 0a86cdf25..5c69a4133 100644 --- a/core/src/main/java/org/nzbhydra/update/UpdateData.java +++ b/core/src/main/java/org/nzbhydra/update/UpdateData.java @@ -1,15 +1,22 @@ package org.nzbhydra.update; -import lombok.Data; import org.nzbhydra.mapping.SemanticVersion; +import org.nzbhydra.springnative.ReflectionMarker; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -@Data +@ReflectionMarker public class UpdateData implements Serializable { - protected List ignoreVersions = new ArrayList<>(); + private List ignoreVersions = new ArrayList<>(); + public List getIgnoreVersions() { + return ignoreVersions; + } + + public void setIgnoreVersions(List ignoreVersions) { + this.ignoreVersions = ignoreVersions; + } } diff --git a/core/src/main/java/org/nzbhydra/update/UpdateManager.java b/core/src/main/java/org/nzbhydra/update/UpdateManager.java index 232d8337e..3496c9ab7 100644 --- a/core/src/main/java/org/nzbhydra/update/UpdateManager.java +++ b/core/src/main/java/org/nzbhydra/update/UpdateManager.java @@ -2,9 +2,6 @@ package org.nzbhydra.update; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.google.common.base.Charsets; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; @@ -14,6 +11,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.io.FileUtils; +import org.nzbhydra.Jackson; import org.nzbhydra.NzbHydra; import org.nzbhydra.backup.BackupAndRestore; import org.nzbhydra.config.BaseConfig; @@ -25,6 +23,7 @@ import org.nzbhydra.mapping.changelog.ChangelogVersionEntry; import org.nzbhydra.mapping.github.Asset; import org.nzbhydra.mapping.github.Release; import org.nzbhydra.notifications.UpdateNotificationEvent; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.systemcontrol.SystemControl; import org.nzbhydra.webaccess.WebAccess; import org.slf4j.Logger; @@ -95,17 +94,10 @@ public class UpdateManager implements InitializingBean { private PackageInfo packageInfo; - private final ObjectMapper objectMapper; protected Supplier> releasesCache = Suppliers.memoizeWithExpiration(getReleasesSupplier(), CACHE_DURATION_MINUTES, TimeUnit.MINUTES); - protected TypeReference> changelogEntryListTypeReference = new TypeReference>() { + protected TypeReference> changelogEntryListTypeReference = new TypeReference<>() { }; - public UpdateManager() { - objectMapper = new ObjectMapper(); - objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } - private void loadPackageInfo() { File packageFile = new File("/app/nzbhydra2/package_info"); if (packageFile.exists()) { @@ -169,16 +161,6 @@ public class UpdateManager implements InitializingBean { updateInfo.setBetaVersion(latestVersionWithBeta.getAsString()); } } - if (currentVersion.major == 4 && latestVersion.major == 5 && !DebugInfosProvider.isRunInDocker()) { - if (!genericStorage.get("MANUAL_UPDATE_5x", Boolean.class).orElse(false)) { - logger.info("An automatic update from 4.x to 5.x is not possible. Please update as explained here: https://github.com/theotherp/nzbhydra2/wiki/Updating-from-4.x-to-5.x"); - } - updateInfo.setUpdateAvailable(false); - updateInfo.setBetaUpdateAvailable(false); - genericStorage.save("MANUAL_UPDATE_5x", true); - } else { - genericStorage.save("MANUAL_UPDATE_5x", false); - } updateInfo.setPackageInfo(getPackageInfo()); return updateInfo; @@ -245,7 +227,7 @@ public class UpdateManager implements InitializingBean { List allChanges; try { String response = webAccess.callUrl(changelogUrl); - allChanges = objectMapper.readValue(response, new TypeReference>() { + allChanges = Jackson.YAML_MAPPER.readValue(response, new TypeReference<>() { }); } catch (IOException e) { throw new UpdateException("Error while getting changelog: " + e.getMessage()); @@ -256,7 +238,7 @@ public class UpdateManager implements InitializingBean { return upToVersion.isSameOrNewer(changeVersion) && changeVersion.isUpdateFor(currentVersion); } - ).sorted(Comparator.reverseOrder()).collect(Collectors.toList()); + ).sorted(Comparator.reverseOrder()).toList(); return collectedVersionChanges.stream() .sorted(Comparator.reverseOrder()) @@ -269,8 +251,8 @@ public class UpdateManager implements InitializingBean { public List getAllVersionChangesUpToCurrentVersion() throws UpdateException { List changelogVersionEntries; try { - String changelogJsonString = Resources.toString(Resources.getResource(UpdateManager.class, "/changelog.json"), Charsets.UTF_8); - changelogVersionEntries = objectMapper.readValue(changelogJsonString, changelogEntryListTypeReference); + String changelogYamlString = Resources.toString(Resources.getResource(UpdateManager.class, "/changelog.yaml"), Charsets.UTF_8); + changelogVersionEntries = Jackson.YAML_MAPPER.readValue(changelogYamlString, changelogEntryListTypeReference); } catch (IOException e) { throw new UpdateException("Error while getting changelog: " + e.getMessage()); } @@ -302,15 +284,15 @@ public class UpdateManager implements InitializingBean { */ public List getAutomaticUpdateVersionHistory() throws UpdateException { Optional previousVersion = genericStorage.get(AutomaticUpdater.TO_NOTICE_KEY, String.class); - if (!previousVersion.isPresent()) { + if (previousVersion.isEmpty()) { logger.error("Unable to find the version from which the automatic update was installed"); return Collections.emptyList(); } List changelogVersionEntries; try { - String changelogJsonString = Resources.toString(Resources.getResource(UpdateManager.class, "/changelog.json"), Charsets.UTF_8); - changelogVersionEntries = objectMapper.readValue(changelogJsonString, changelogEntryListTypeReference); + String changelogJsonString = Resources.toString(Resources.getResource(UpdateManager.class, "/changelog.yaml"), Charsets.UTF_8); + changelogVersionEntries = Jackson.YAML_MAPPER.readValue(changelogJsonString, changelogEntryListTypeReference); } catch (IOException e) { throw new UpdateException("Error while getting changelog: " + e.getMessage()); } @@ -363,16 +345,12 @@ public class UpdateManager implements InitializingBean { try { logger.info("Creating backup before shutting down"); applicationEventPublisher.publishEvent(new UpdateEvent(UpdateEvent.State.CREATING_BACKUP, "Creating backup before update.")); - backupAndRestore.backup(); + backupAndRestore.backup(false); } catch (Exception e) { throw new UpdateException("Unable to create backup before update", e); } } - if (release.getTagName().equals("v2.7.6")) { - applicationEventPublisher.publishEvent(new UpdateEvent(UpdateEvent.State.MIGRATION_NEEDED, "NZBHydra's restart after the update will take longer than usual because the database needs to be migrated.")); - } - if (isAutomaticUpdate) { genericStorage.save("automaticUpdateToNotice", getCurrentVersionString()); } @@ -394,7 +372,7 @@ public class UpdateManager implements InitializingBean { boolean isOsWindows = osName.toLowerCase().contains("windows"); String assetToContain = isOsWindows ? "windows" : "linux"; //LATER What about OSX? Optional optionalAsset = assets.stream().filter(x -> x.getName().toLowerCase().contains(assetToContain)).findFirst(); - if (!optionalAsset.isPresent()) { + if (optionalAsset.isEmpty()) { logger.error("Unable to find asset for platform {} in these assets: {}", assetToContain, assets.stream().map(Asset::getName).collect(Collectors.joining(", "))); throw new UpdateException("Unable to find asset for current platform " + assetToContain); } @@ -406,7 +384,7 @@ public class UpdateManager implements InitializingBean { try { String url = repositoryBaseUrl + "/releases"; logger.debug("Retrieving latest release from GitHub using URL {}", url); - List releases = webAccess.callUrl(url, new TypeReference>() { + List releases = webAccess.callUrl(url, new TypeReference<>() { }); return releases; } catch (IOException e) { @@ -431,7 +409,7 @@ public class UpdateManager implements InitializingBean { List blockedVersions; try { String response = webAccess.callUrl(blockedVersionsUrl); - blockedVersions = objectMapper.readValue(response, new TypeReference>() { + blockedVersions = Jackson.YAML_MAPPER.readValue(response, new TypeReference<>() { }); } catch (IOException e) { throw new UpdateException("Error while getting blocked versions: " + e.getMessage()); @@ -460,6 +438,7 @@ public class UpdateManager implements InitializingBean { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class UpdateEvent { @@ -476,6 +455,7 @@ public class UpdateManager implements InitializingBean { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class BlockedVersion { @@ -484,6 +464,7 @@ public class UpdateManager implements InitializingBean { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class PackageInfo { @@ -493,6 +474,7 @@ public class UpdateManager implements InitializingBean { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class UpdateInfo { diff --git a/core/src/main/java/org/nzbhydra/update/UpdatesWeb.java b/core/src/main/java/org/nzbhydra/update/UpdatesWeb.java index 842f648fc..9eed8f74d 100644 --- a/core/src/main/java/org/nzbhydra/update/UpdatesWeb.java +++ b/core/src/main/java/org/nzbhydra/update/UpdatesWeb.java @@ -12,6 +12,7 @@ import org.nzbhydra.genericstorage.GenericStorage; import org.nzbhydra.mapping.SemanticVersion; import org.nzbhydra.mapping.changelog.ChangelogVersionEntry; import org.nzbhydra.problemdetection.OutdatedWrapperDetector; +import org.nzbhydra.springnative.ReflectionMarker; import org.nzbhydra.update.UpdateManager.UpdateEvent; import org.nzbhydra.web.SessionStorage; import org.slf4j.Logger; @@ -182,6 +183,7 @@ public class UpdatesWeb { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class VersionsInfo { diff --git a/tests/src/test/java/org/nzbhydra/tests/pageobjects/IIndexerSelectionButton.java b/core/src/main/java/org/nzbhydra/web/ApiError.java similarity index 69% rename from tests/src/test/java/org/nzbhydra/tests/pageobjects/IIndexerSelectionButton.java rename to core/src/main/java/org/nzbhydra/web/ApiError.java index 4a7c858c0..fd551211f 100644 --- a/tests/src/test/java/org/nzbhydra/tests/pageobjects/IIndexerSelectionButton.java +++ b/core/src/main/java/org/nzbhydra/web/ApiError.java @@ -1,5 +1,5 @@ /* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.nzbhydra.tests.pageobjects; +package org.nzbhydra.web; - public interface IIndexerSelectionButton extends ISelectionButton{ +import org.nzbhydra.springnative.ReflectionMarker; - void reset(); +import java.time.Instant; - void selectAllUsenet(); +@ReflectionMarker +public record ApiError(String status, Instant timestamp, String message) { - void selectAllTorrent(); } diff --git a/core/src/main/java/org/nzbhydra/web/BootstrappedDataTO.java b/core/src/main/java/org/nzbhydra/web/BootstrappedDataTO.java index 31ffdcc63..0c2b9d815 100644 --- a/core/src/main/java/org/nzbhydra/web/BootstrappedDataTO.java +++ b/core/src/main/java/org/nzbhydra/web/BootstrappedDataTO.java @@ -2,8 +2,10 @@ package org.nzbhydra.web; import lombok.Data; import org.nzbhydra.config.safeconfig.SafeConfig; +import org.nzbhydra.springnative.ReflectionMarker; @Data +@ReflectionMarker public class BootstrappedDataTO { private String username; diff --git a/core/src/main/java/org/nzbhydra/web/ErrorHandler.java b/core/src/main/java/org/nzbhydra/web/ErrorHandler.java index 018da5b59..bb1463545 100644 --- a/core/src/main/java/org/nzbhydra/web/ErrorHandler.java +++ b/core/src/main/java/org/nzbhydra/web/ErrorHandler.java @@ -18,10 +18,12 @@ package org.nzbhydra.web; import com.google.common.base.Joiner; import com.google.common.collect.Sets; +import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.nzbhydra.springnative.ReflectionMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.ConversionNotSupportedException; @@ -49,7 +51,6 @@ import org.springframework.web.context.request.async.AsyncRequestTimeoutExceptio import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.NoHandlerFoundException; -import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; @@ -195,6 +196,7 @@ public class ErrorHandler { } @Data +@ReflectionMarker @AllArgsConstructor @NoArgsConstructor public static class JsonExceptionResponse { diff --git a/core/src/main/java/org/nzbhydra/web/HelpWeb.java b/core/src/main/java/org/nzbhydra/web/HelpWeb.java index abe7e65ff..d3dab7d23 100644 --- a/core/src/main/java/org/nzbhydra/web/HelpWeb.java +++ b/core/src/main/java/org/nzbhydra/web/HelpWeb.java @@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController public class HelpWeb { - @RequestMapping(value = "internalapi/help/{section}", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE) + @RequestMapping(value = "/internalapi/help/{section}", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE) @Secured({"ROLE_ADMIN"}) public String askForAdmin(@PathVariable String section) throws Exception { String markdown = Resources.toString(Resources.getResource(HelpWeb.class, ("/help/" + section + ".md").toLowerCase()), Charsets.UTF_8); diff --git a/core/src/main/java/org/nzbhydra/web/HydraErrorController.java b/core/src/main/java/org/nzbhydra/web/HydraErrorController.java new file mode 100644 index 000000000..3ddd233a6 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/web/HydraErrorController.java @@ -0,0 +1,47 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.web; + +import com.google.common.base.Throwables; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.time.Instant; + +@Controller +public class HydraErrorController extends AbstractErrorController implements ErrorController { + public HydraErrorController(ErrorAttributes errorAttributes) { + super(errorAttributes); + } + + @RequestMapping("/error") + public ModelAndView handleError(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + //do something like logging + ModelAndView errorPage = new ModelAndView("error"); + errorPage.addObject("exception", Throwables.getStackTraceAsString(ex)); + errorPage.addObject("error", ex.getMessage()); + errorPage.addObject("status", response.getStatus()); + errorPage.addObject("timestamp", Instant.now().toString()); + return errorPage; + } +} diff --git a/core/src/main/java/org/nzbhydra/web/HydraRestControllerAdvice.java b/core/src/main/java/org/nzbhydra/web/HydraRestControllerAdvice.java new file mode 100644 index 000000000..b5d1fbb72 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/web/HydraRestControllerAdvice.java @@ -0,0 +1,36 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.web; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.Instant; + +@RestControllerAdvice +public class HydraRestControllerAdvice { + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ResponseBody + public ApiError handleCustomException(Exception ce) { + return new ApiError(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), Instant.now(), ce.getMessage()); + } +} diff --git a/core/src/main/java/org/nzbhydra/web/Interceptor.java b/core/src/main/java/org/nzbhydra/web/Interceptor.java index 629bf6251..762187e17 100644 --- a/core/src/main/java/org/nzbhydra/web/Interceptor.java +++ b/core/src/main/java/org/nzbhydra/web/Interceptor.java @@ -1,6 +1,8 @@ package org.nzbhydra.web; import com.google.common.base.Strings; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.misc.UserAgentMapper; import org.slf4j.Logger; @@ -8,15 +10,13 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.HandlerInterceptor; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.net.InetAddress; import java.net.UnknownHostException; @Component -public class Interceptor extends HandlerInterceptorAdapter { +public class Interceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(Interceptor.class); @Autowired diff --git a/core/src/main/java/org/nzbhydra/web/MainWeb.java b/core/src/main/java/org/nzbhydra/web/MainWeb.java index 539205982..5cc031550 100644 --- a/core/src/main/java/org/nzbhydra/web/MainWeb.java +++ b/core/src/main/java/org/nzbhydra/web/MainWeb.java @@ -1,5 +1,8 @@ package org.nzbhydra.web; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import org.nzbhydra.auth.UserInfosProvider; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.safeconfig.SafeConfig; @@ -10,9 +13,6 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import java.security.Principal; import java.util.Arrays; @@ -30,7 +30,6 @@ public class MainWeb { return new SafeConfig(configProvider.getBaseConfig()); } - @RequestMapping(value = "/", method = RequestMethod.GET) @Secured({"ROLE_USER"}) public String index(HttpSession session, Principal principal, HttpServletResponse response) { @@ -40,7 +39,7 @@ public class MainWeb { } //Must exist and not be protected so that redirects to "/login" have a target - @RequestMapping(value = "/login", method = RequestMethod.GET) + @RequestMapping(value = "/login", method = {RequestMethod.GET, RequestMethod.PUT}) public String index2(HttpSession session, Principal principal) { setSessionAttributes(session, principal); return "login"; diff --git a/core/src/main/java/org/nzbhydra/web/UrlCalculator.java b/core/src/main/java/org/nzbhydra/web/UrlCalculator.java index 63b393f81..6163111c4 100644 --- a/core/src/main/java/org/nzbhydra/web/UrlCalculator.java +++ b/core/src/main/java/org/nzbhydra/web/UrlCalculator.java @@ -18,6 +18,7 @@ package org.nzbhydra.web; import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.Strings; +import jakarta.servlet.http.HttpServletRequest; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; import org.nzbhydra.debuginfos.DebugInfosProvider; @@ -32,7 +33,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.util.UriComponentsBuilder; -import javax.servlet.http.HttpServletRequest; import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; @@ -42,7 +42,6 @@ import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import java.util.stream.Stream; @Component @@ -179,10 +178,10 @@ public class UrlCalculator { try { InetAddress candidateAddress = null; final List networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()).stream() - .filter(i -> Stream.of("VirtualBox", "Hyper-V", "Bluetooth", "Miniport").noneMatch(name -> i.getDisplayName() != null && i.getDisplayName().contains(name))) - .filter(i -> i.getInetAddresses().hasMoreElements()) - .filter(i -> !(i.getInetAddresses().nextElement() instanceof Inet6Address)) - .collect(Collectors.toList()); + .filter(i -> Stream.of("VirtualBox", "Hyper-V", "Bluetooth", "Miniport").noneMatch(name -> i.getDisplayName() != null && i.getDisplayName().contains(name))) + .filter(i -> i.getInetAddresses().hasMoreElements()) + .filter(i -> !(i.getInetAddresses().nextElement() instanceof Inet6Address)) + .toList(); for (NetworkInterface iface : networkInterfaces) { for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) { diff --git a/core/src/main/java/org/nzbhydra/web/WebConfiguration.java b/core/src/main/java/org/nzbhydra/web/WebConfiguration.java index a6295ad9f..17a06b085 100644 --- a/core/src/main/java/org/nzbhydra/web/WebConfiguration.java +++ b/core/src/main/java/org/nzbhydra/web/WebConfiguration.java @@ -1,6 +1,7 @@ package org.nzbhydra.web; import com.fasterxml.jackson.databind.module.SimpleModule; +import jakarta.xml.bind.Marshaller; import org.nzbhydra.NzbHydra; import org.nzbhydra.api.stats.HistoryRequestConverter; import org.nzbhydra.api.stats.StatsRequestConverter; @@ -34,7 +35,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupp import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.resource.ResourceUrlProvider; -import javax.xml.bind.Marshaller; import javax.xml.transform.stream.StreamResult; import java.io.ByteArrayOutputStream; import java.io.File; @@ -137,8 +137,7 @@ public class WebConfiguration extends WebMvcConfigurationSupport { @Override protected void extendMessageConverters(List> converters) { for (HttpMessageConverter converter : converters) { - if (converter instanceof MappingJackson2HttpMessageConverter) { - MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter; + if (converter instanceof MappingJackson2HttpMessageConverter jacksonConverter) { jacksonConverter.setPrettyPrint(true); SimpleModule simpleModule = new SimpleModule(); simpleModule.addDeserializer(String.class, new EmptyStringToNullDeserializer()); diff --git a/core/src/main/java/org/nzbhydra/web/WebSocketConfig.java b/core/src/main/java/org/nzbhydra/web/WebSocketConfig.java index 15dbf3780..e33b5db1b 100644 --- a/core/src/main/java/org/nzbhydra/web/WebSocketConfig.java +++ b/core/src/main/java/org/nzbhydra/web/WebSocketConfig.java @@ -16,9 +16,8 @@ package org.nzbhydra.web; -import org.nzbhydra.ShutdownEvent; +import jakarta.annotation.PreDestroy; import org.springframework.context.annotation.Configuration; -import org.springframework.context.event.EventListener; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -40,7 +39,7 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/websocket/**") + registry.addEndpoint("/websocket") .setAllowedOriginPatterns("*") .withSockJS() .setClientLibraryUrl("//cdn.jsdelivr.net/sockjs/1.0.3/sockjs.min.js"); @@ -63,8 +62,8 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { registration.taskExecutor(taskExecutor); } - @EventListener - public void onShutdown(ShutdownEvent event) { + @PreDestroy + public void onShutdown() { taskExecutor.shutdown(); } diff --git a/core/src/main/java/org/nzbhydra/web/WelcomeWeb.java b/core/src/main/java/org/nzbhydra/web/WelcomeWeb.java index c34b16d4a..4f45a6860 100644 --- a/core/src/main/java/org/nzbhydra/web/WelcomeWeb.java +++ b/core/src/main/java/org/nzbhydra/web/WelcomeWeb.java @@ -1,5 +1,6 @@ package org.nzbhydra.web; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +18,8 @@ public class WelcomeWeb { @Autowired private ConfigProvider configProvider; + @Autowired + private BaseConfigHandler baseConfigHandler; private static final Logger logger = LoggerFactory.getLogger(WelcomeWeb.class); @@ -31,7 +34,7 @@ public class WelcomeWeb { public void setWelcomeShown() throws IOException { logger.debug("Welcome screen was shown"); configProvider.getBaseConfig().getMain().setWelcomeShown(true); - configProvider.getBaseConfig().save(true); + baseConfigHandler.save(true); } } diff --git a/core/src/main/java/org/nzbhydra/webaccess/AbstractAsyncClientHttpRequest.java b/core/src/main/java/org/nzbhydra/webaccess/AbstractAsyncClientHttpRequest.java deleted file mode 100644 index 16b9ce01a..000000000 --- a/core/src/main/java/org/nzbhydra/webaccess/AbstractAsyncClientHttpRequest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2002-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.webaccess; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.client.AsyncClientHttpRequest; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * Abstract base for {@link AsyncClientHttpRequest} that makes sure that headers and body - * are not written multiple times. - * - * @author Arjen Poutsma - * @since 4.0 - */ -abstract class AbstractAsyncClientHttpRequest implements AsyncClientHttpRequest { - - private final HttpHeaders headers = new HttpHeaders(); - - private boolean executed = false; - - - @Override - public final HttpHeaders getHeaders() { - return (this.executed ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public final OutputStream getBody() throws IOException { - assertNotExecuted(); - return getBodyInternal(this.headers); - } - - @Override - public ListenableFuture executeAsync() throws IOException { - assertNotExecuted(); - ListenableFuture result = executeInternal(this.headers); - this.executed = true; - return result; - } - - /** - * Asserts that this request has not been {@linkplain #executeAsync() executed} yet. - * - * @throws IllegalStateException if this request has been executed - */ - protected void assertNotExecuted() { - Assert.state(!this.executed, "ClientHttpRequest already executed"); - } - - - /** - * Abstract template method that returns the body. - * - * @param headers the HTTP headers - * @return the body output stream - */ - protected abstract OutputStream getBodyInternal(HttpHeaders headers) throws IOException; - - /** - * Abstract template method that writes the given headers and content to the HTTP request. - * - * @param headers the HTTP headers - * @return the response object for the executed request - */ - protected abstract ListenableFuture executeInternal(HttpHeaders headers) - throws IOException; - -} diff --git a/core/src/main/java/org/nzbhydra/webaccess/AbstractBufferingAsyncClientHttpRequest.java b/core/src/main/java/org/nzbhydra/webaccess/AbstractBufferingAsyncClientHttpRequest.java deleted file mode 100644 index 280250374..000000000 --- a/core/src/main/java/org/nzbhydra/webaccess/AbstractBufferingAsyncClientHttpRequest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.webaccess; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.client.AsyncClientHttpRequest; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.util.concurrent.ListenableFuture; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * Base implementation of {@link AsyncClientHttpRequest} that buffers output - * in a byte array before sending it over the wire. - * - * @author Arjen Poutsma - * @since 4.0 - */ -abstract class AbstractBufferingAsyncClientHttpRequest extends AbstractAsyncClientHttpRequest { - - private ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream(1024); - - - @Override - protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException { - return this.bufferedOutput; - } - - @Override - protected ListenableFuture executeInternal(HttpHeaders headers) throws IOException { - byte[] bytes = this.bufferedOutput.toByteArray(); - if (headers.getContentLength() < 0) { - headers.setContentLength(bytes.length); - } - ListenableFuture result = executeInternal(headers, bytes); - this.bufferedOutput = null; - return result; - } - - /** - * Abstract template method that writes the given headers and content to the HTTP request. - * - * @param headers the HTTP headers - * @param bufferedOutput the body content - * @return the response object for the executed request - */ - protected abstract ListenableFuture executeInternal( - HttpHeaders headers, byte[] bufferedOutput) throws IOException; - -} diff --git a/core/src/main/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactory.java b/core/src/main/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactory.java index 32801f8f2..b09c371df 100644 --- a/core/src/main/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactory.java +++ b/core/src/main/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactory.java @@ -17,6 +17,7 @@ package org.nzbhydra.webaccess; import com.google.common.net.InetAddresses; +import jakarta.annotation.PostConstruct; import joptsimple.internal.Strings; import okhttp3.ConnectionPool; import okhttp3.Credentials; @@ -27,10 +28,11 @@ import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.Route; import okhttp3.logging.HttpLoggingInterceptor; +import org.apache.commons.lang3.tuple.Pair; import org.nzbhydra.config.ConfigChangedEvent; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.MainConfig; -import org.nzbhydra.config.downloading.ProxyType; +import org.nzbhydra.config.ProxyType; import org.nzbhydra.logging.LoggingMarkers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,8 +42,6 @@ import org.springframework.context.annotation.Primary; import org.springframework.context.event.EventListener; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.client.AsyncClientHttpRequest; -import org.springframework.http.client.AsyncClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.stereotype.Component; @@ -50,7 +50,6 @@ import sockslib.client.Socks5; import sockslib.client.SocksProxy; import sockslib.client.SocksSocket; -import javax.annotation.PostConstruct; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; @@ -63,6 +62,7 @@ import java.net.Proxy.Type; import java.net.Socket; import java.net.URI; import java.net.UnknownHostException; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -71,7 +71,7 @@ import static org.nzbhydra.webaccess.Ssl.isSameHost; @Component @Primary -public class HydraOkHttp3ClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { +public class HydraOkHttp3ClientHttpRequestFactory implements ClientHttpRequestFactory { @Value("${nzbhydra.connectionTimeout:10}") private int timeout; @@ -86,6 +86,8 @@ public class HydraOkHttp3ClientHttpRequestFactory implements ClientHttpRequestFa private HttpLoggingInterceptor httpLoggingInterceptor; private SocketFactory sockProxySocketFactory; + private Map, OkHttpClient> clientCache = new HashMap<>(); + @PostConstruct public void init() { MainConfig mainConfig = configProvider.getBaseConfig().getMain(); @@ -101,12 +103,7 @@ public class HydraOkHttp3ClientHttpRequestFactory implements ClientHttpRequestFa @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { - return new OkHttp3ClientHttpRequest(getOkHttpClientBuilder(uri).build(), uri, httpMethod); - } - - @Override - public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) { - return new OkHttp3AsyncClientHttpRequest(getOkHttpClientBuilder(uri).build(), uri, httpMethod); + return new OkHttp3ClientHttpRequest(getOkHttpClient(uri.getHost()), uri, httpMethod); } @@ -133,18 +130,18 @@ public class HydraOkHttp3ClientHttpRequestFactory implements ClientHttpRequestFa return (StringUtils.hasText(rawContentType) ? okhttp3.MediaType.parse(rawContentType) : null); } - public Builder getOkHttpClientBuilder(URI requestUri) { + protected Builder getOkHttpClientBuilder(String host) { Builder builder = getBaseBuilder(); - configureBuilderForSsl(requestUri, builder); + configureBuilderForSsl(builder, host); MainConfig main = configProvider.getBaseConfig().getMain(); if (main.getProxyType() == ProxyType.NONE) { return builder; } - if (isUriToBeIgnoredByProxy(requestUri.getHost())) { - logger.debug("Not using proxy for request to {}", requestUri.getHost()); + if (isUriToBeIgnoredByProxy(host)) { + logger.debug("Not using proxy for request to {}", host); return builder; } @@ -162,15 +159,31 @@ public class HydraOkHttp3ClientHttpRequestFactory implements ClientHttpRequestFa String credential = Credentials.basic(main.getProxyUsername(), main.getProxyPassword()); return response.request().newBuilder() - .header("Proxy-Authorization", credential).build(); + .header("Proxy-Authorization", credential).build(); }); } } return builder; } - void configureBuilderForSsl(URI requestUri, Builder builder) { - String host = requestUri.getHost(); + public OkHttpClient getOkHttpClient(String host) { + return getOkHttpClient(host, null); + } + + public OkHttpClient getOkHttpClient(String host, Integer timeout) { + return clientCache.computeIfAbsent(Pair.of(host, timeout), pair -> { + final Builder clientBuilder = getOkHttpClientBuilder(host); + if (timeout == null) { + return clientBuilder.build(); + } + return clientBuilder + .readTimeout(timeout, TimeUnit.SECONDS) + .connectTimeout(timeout, TimeUnit.SECONDS) + .writeTimeout(timeout, TimeUnit.SECONDS).build(); + }); + } + + void configureBuilderForSsl(Builder builder, String host) { final Ssl.SslVerificationState verificationState = ssl.getVerificationStateForHost(host); SSLSocketFactory allTrustingSslSocketFactory = ssl.getAllTrustingSslSocketFactory(); X509TrustManager allTrustingDefaultTrustManager = ssl.getAllTrustingDefaultTrustManager(); @@ -178,10 +191,10 @@ public class HydraOkHttp3ClientHttpRequestFactory implements ClientHttpRequestFa builder.sslSocketFactory(ssl.getDefaultSslSocketFactory(), ssl.getDefaultTrustManager()); } else if (verificationState == Ssl.SslVerificationState.DISABLED_HOST) { builder.sslSocketFactory(allTrustingSslSocketFactory, allTrustingDefaultTrustManager) - .hostnameVerifier((hostname, session) -> { - logger.debug(LoggingMarkers.HTTPS, "Not verifying host name {}", hostname); - return true; - }); + .hostnameVerifier((hostname, session) -> { + logger.debug(LoggingMarkers.HTTPS, "Not verifying host name {}", hostname); + return true; + }); } else { builder.sslSocketFactory(allTrustingSslSocketFactory, allTrustingDefaultTrustManager); } diff --git a/core/src/main/java/org/nzbhydra/webaccess/OkHttp3AsyncClientHttpRequest.java b/core/src/main/java/org/nzbhydra/webaccess/OkHttp3AsyncClientHttpRequest.java deleted file mode 100644 index 2ce091e75..000000000 --- a/core/src/main/java/org/nzbhydra/webaccess/OkHttp3AsyncClientHttpRequest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2002-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.webaccess; - -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.AsyncClientHttpRequest; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; - -import java.io.IOException; -import java.net.URI; - -/** - * {@link AsyncClientHttpRequest} implementation based on OkHttp 3.x. - *

- *

Created via the {@link OkHttp3ClientHttpRequestFactory}. - * - * @author Luciano Leggieri - * @author Arjen Poutsma - * @author Roy Clarkson - * @since 4.3 - */ -class OkHttp3AsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { - - private final OkHttpClient client; - - private final URI uri; - - private final HttpMethod method; - - - public OkHttp3AsyncClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { - this.client = client; - this.uri = uri; - this.method = method; - } - - - @Override - public HttpMethod getMethod() { - return this.method; - } - - @Override - public String getMethodValue() { - return this.method.name(); - } - - @Override - public URI getURI() { - return this.uri; - } - - @Override - protected ListenableFuture executeInternal(HttpHeaders headers, byte[] content) - throws IOException { - - Request request = HydraOkHttp3ClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); - return new OkHttpListenableFuture(this.client.newCall(request)); - } - - - private static class OkHttpListenableFuture extends SettableListenableFuture { - - private final Call call; - - public OkHttpListenableFuture(Call call) { - this.call = call; - this.call.enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - set(new OkHttp3ClientHttpResponse(response)); - } - - @Override - public void onFailure(Call call, IOException ex) { - setException(ex); - } - }); - } - - @Override - protected void interruptTask() { - this.call.cancel(); - } - } - -} diff --git a/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpRequest.java b/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpRequest.java index ab66c915c..c6e4fcda6 100644 --- a/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpRequest.java +++ b/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpRequest.java @@ -58,11 +58,6 @@ class OkHttp3ClientHttpRequest extends AbstractBufferingClientHttpRequest { return this.method; } - @Override - public String getMethodValue() { - return method.name(); - } - @Override public URI getURI() { return this.uri; diff --git a/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpResponse.java b/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpResponse.java index c8f6859ef..3778689df 100644 --- a/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpResponse.java +++ b/core/src/main/java/org/nzbhydra/webaccess/OkHttp3ClientHttpResponse.java @@ -78,6 +78,7 @@ class OkHttp3ClientHttpResponse extends AbstractClientHttpResponse { @Override public void close() { this.response.body().close(); + this.response.close(); } } diff --git a/core/src/main/java/org/nzbhydra/webaccess/Ssl.java b/core/src/main/java/org/nzbhydra/webaccess/Ssl.java index ba1fe9a49..0df5e86c2 100644 --- a/core/src/main/java/org/nzbhydra/webaccess/Ssl.java +++ b/core/src/main/java/org/nzbhydra/webaccess/Ssl.java @@ -16,6 +16,7 @@ package org.nzbhydra.webaccess; +import jakarta.annotation.PostConstruct; import org.nzbhydra.NzbHydra; import org.nzbhydra.config.ConfigChangedEvent; import org.nzbhydra.config.ConfigProvider; @@ -28,7 +29,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; @@ -219,7 +219,7 @@ public class Ssl { return false; } - return aMatcher.group(3).toLowerCase().equals(bMatcher.group(3).toLowerCase()); + return aMatcher.group(3).equalsIgnoreCase(bMatcher.group(3)); } //https://stackoverflow.com/a/2406819/184264 diff --git a/core/src/main/java/org/nzbhydra/webaccess/WebAccess.java b/core/src/main/java/org/nzbhydra/webaccess/WebAccess.java index cd8c9d36d..99a922b6d 100644 --- a/core/src/main/java/org/nzbhydra/webaccess/WebAccess.java +++ b/core/src/main/java/org/nzbhydra/webaccess/WebAccess.java @@ -28,7 +28,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; -@SuppressWarnings("ConstantConditions") @Component public class WebAccess { @@ -84,7 +83,9 @@ public class WebAccess { Request request = builder.build(); - OkHttpClient client = requestFactory.getOkHttpClientBuilder(request.url().uri()).readTimeout(timeout, TimeUnit.SECONDS).connectTimeout(timeout, TimeUnit.SECONDS).writeTimeout(timeout, TimeUnit.SECONDS).build(); + OkHttpClient client = requestFactory.getOkHttpClient(request.url().uri().getHost(), timeout); + + String bodyAsString; try (Response response = client.newCall(request).execute(); ResponseBody body = response.body()) { try { @@ -120,7 +121,7 @@ public class WebAccess { logger.debug("Downloading file from {} to {}", url, file.getAbsolutePath()); Stopwatch stopwatch = Stopwatch.createStarted(); Request request = new Request.Builder().url(url).build(); - try (Response response = requestFactory.getOkHttpClientBuilder(request.url().uri()).build().newCall(request).execute(); ResponseBody body = response.body()) { + try (Response response = requestFactory.getOkHttpClient(request.url().uri().getHost()).newCall(request).execute(); ResponseBody body = response.body()) { long contentLength = body.contentLength(); if (!response.isSuccessful()) { String error = String.format("URL call to %s returned %d:%s", url, response.code(), response.message()); diff --git a/core/src/main/java/org/springframework/boot/SpringApplicationAotProcessor.java b/core/src/main/java/org/springframework/boot/SpringApplicationAotProcessor.java new file mode 100644 index 000000000..c06ead6da --- /dev/null +++ b/core/src/main/java/org/springframework/boot/SpringApplicationAotProcessor.java @@ -0,0 +1,103 @@ +/* +The original from Spring Boot does not return for some reason. + */ + +package org.springframework.boot; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.aot.ContextAotProcessor; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.function.ThrowingSupplier; + +import java.lang.reflect.Method; +import java.nio.file.Paths; +import java.util.Arrays; + +public class SpringApplicationAotProcessor extends ContextAotProcessor { + + private final String[] applicationArgs; + + /** + * Create a new processor for the specified application and settings. + * @param application the application main class + * @param settings the general AOT processor settings + * @param applicationArgs the arguments to provide to the main method + */ + public SpringApplicationAotProcessor(Class application, Settings settings, String[] applicationArgs) { + super(application, settings); + this.applicationArgs = applicationArgs; + } + + @Override + protected GenericApplicationContext prepareApplicationContext(Class application) { + return new AotProcessorHook(application).run(() -> { + Method mainMethod = application.getMethod("main", String[].class); + return ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { this.applicationArgs }); + }); + } + + public static void main(String[] args) throws Exception { + System.out.println("Using My org.springframework.boot.SpringApplicationAotProcessor"); + int requiredArgs = 6; + Assert.isTrue(args.length >= requiredArgs, () -> "Usage: " + SpringApplicationAotProcessor.class.getName() + + " "); + Class application = Class.forName(args[0]); + Settings settings = Settings.builder().sourceOutput(Paths.get(args[1])).resourceOutput(Paths.get(args[2])) + .classOutput(Paths.get(args[3])).groupId((StringUtils.hasText(args[4])) ? args[4] : "unspecified") + .artifactId(args[5]).build(); + String[] applicationArgs = (args.length > requiredArgs) ? Arrays.copyOfRange(args, requiredArgs, args.length) + : new String[0]; + new SpringApplicationAotProcessor(application, settings, applicationArgs).process(); + + //Only difference to original + System.exit(0); + } + + /** + * {@link SpringApplicationHook} used to capture the {@link ApplicationContext} and + * trigger early exit of main method. + */ + private static final class AotProcessorHook implements SpringApplicationHook { + + private final Class application; + + private AotProcessorHook(Class application) { + this.application = application; + } + + @Override + public SpringApplicationRunListener getRunListener(SpringApplication application) { + return new SpringApplicationRunListener() { + + @Override + public void contextLoaded(ConfigurableApplicationContext context) { + throw new SpringApplication.AbandonedRunException(context); + } + + }; + } + + private GenericApplicationContext run(ThrowingSupplier action) { + try { + SpringApplication.withHook(this, action); + } + catch (SpringApplication.AbandonedRunException ex) { + ApplicationContext context = ex.getApplicationContext(); + Assert.isInstanceOf(GenericApplicationContext.class, context, + () -> "AOT processing requires a GenericApplicationContext but got a " + + context.getClass().getName()); + return (GenericApplicationContext) context; + } + throw new IllegalStateException( + "No application context available after calling main method of '%s'. Does it run a SpringApplication?" + .formatted(this.application.getName())); + } + + } + +} + diff --git a/core/src/main/resources/META-INF/native-image/jni-config.json b/core/src/main/resources/META-INF/native-image/jni-config.json new file mode 100644 index 000000000..b5b25595f --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/jni-config.json @@ -0,0 +1,752 @@ +[ +{ + "name":"[Lcom.sun.management.internal.DiagnosticCommandArgumentInfo;" +}, +{ + "name":"[Lcom.sun.management.internal.DiagnosticCommandInfo;" +}, +{ + "name":"[Lsun.java2d.loops.GraphicsPrimitive;" +}, +{ + "name":"com.sun.management.internal.DiagnosticCommandArgumentInfo", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","boolean","boolean","boolean","int"] }] +}, +{ + "name":"com.sun.management.internal.DiagnosticCommandInfo", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","boolean","java.util.List"] }] +}, +{ + "name":"java.awt.AWTEvent", + "fields":[ + {"name":"bdata"}, + {"name":"consumed"}, + {"name":"id"} + ] +}, +{ + "name":"java.awt.AlphaComposite", + "fields":[ + {"name":"extraAlpha"}, + {"name":"rule"} + ] +}, +{ + "name":"java.awt.Color", + "methods":[{"name":"getRGB","parameterTypes":[] }] +}, +{ + "name":"java.awt.Component", + "fields":[ + {"name":"appContext"}, + {"name":"background"}, + {"name":"cursor"}, + {"name":"enabled"}, + {"name":"focusable"}, + {"name":"foreground"}, + {"name":"graphicsConfig"}, + {"name":"height"}, + {"name":"parent"}, + {"name":"peer"}, + {"name":"visible"}, + {"name":"width"}, + {"name":"x"}, + {"name":"y"} + ], + "methods":[ + {"name":"getFont_NoClientCode","parameterTypes":[] }, + {"name":"getLocationOnScreen_NoTreeLock","parameterTypes":[] }, + {"name":"getToolkitImpl","parameterTypes":[] }, + {"name":"isEnabledImpl","parameterTypes":[] } + ] +}, +{ + "name":"java.awt.Container", + "fields":[{"name":"layoutMgr"}] +}, +{ + "name":"java.awt.Cursor", + "fields":[ + {"name":"pData"}, + {"name":"type"} + ], + "methods":[{"name":"setPData","parameterTypes":["long"] }] +}, +{ + "name":"java.awt.Font", + "fields":[ + {"name":"name"}, + {"name":"pData"}, + {"name":"size"}, + {"name":"style"} + ], + "methods":[ + {"name":"getFont","parameterTypes":["java.lang.String"] }, + {"name":"getFontPeer","parameterTypes":[] } + ] +}, +{ + "name":"java.awt.Frame", + "fields":[{"name":"undecorated"}] +}, +{ + "name":"java.awt.Insets", + "fields":[ + {"name":"bottom"}, + {"name":"left"}, + {"name":"right"}, + {"name":"top"} + ], + "methods":[{"name":"","parameterTypes":["int","int","int","int"] }] +}, +{ + "name":"java.awt.Menu", + "methods":[ + {"name":"countItemsImpl","parameterTypes":[] }, + {"name":"getItemImpl","parameterTypes":["int"] } + ] +}, +{ + "name":"java.awt.MenuItem", + "fields":[ + {"name":"enabled"}, + {"name":"label"} + ] +}, +{ + "name":"java.awt.Point", + "fields":[ + {"name":"x"}, + {"name":"y"} + ] +}, +{ + "name":"java.awt.Rectangle", + "methods":[{"name":"","parameterTypes":["int","int","int","int"] }] +}, +{ + "name":"java.awt.Toolkit", + "methods":[ + {"name":"getDefaultToolkit","parameterTypes":[] }, + {"name":"getFontMetrics","parameterTypes":["java.awt.Font"] } + ] +}, +{ + "name":"java.awt.TrayIcon", + "fields":[ + {"name":"actionCommand"}, + {"name":"id"} + ] +}, +{ + "name":"java.awt.Window", + "fields":[ + {"name":"autoRequestFocus"}, + {"name":"locationByPlatform"}, + {"name":"securityWarningHeight"}, + {"name":"securityWarningWidth"}, + {"name":"warningString"} + ], + "methods":[ + {"name":"calculateSecurityWarningPosition","parameterTypes":["double","double","double","double"] }, + {"name":"getWarningString","parameterTypes":[] } + ] +}, +{ + "name":"java.awt.Window$Type" +}, +{ + "name":"java.awt.desktop.UserSessionEvent$Reason", + "fields":[ + {"name":"CONSOLE"}, + {"name":"LOCK"}, + {"name":"REMOTE"}, + {"name":"UNSPECIFIED"} + ] +}, +{ + "name":"java.awt.event.ComponentEvent", + "methods":[{"name":"","parameterTypes":["java.awt.Component","int"] }] +}, +{ + "name":"java.awt.event.InputEvent", + "fields":[{"name":"modifiers"}], + "methods":[{"name":"getButtonDownMasks","parameterTypes":[] }] +}, +{ + "name":"java.awt.event.MouseEvent", + "fields":[ + {"name":"button"}, + {"name":"causedByTouchEvent"}, + {"name":"x"}, + {"name":"y"} + ], + "methods":[{"name":"","parameterTypes":["java.awt.Component","int","long","int","int","int","int","int","int","boolean","int"] }] +}, +{ + "name":"java.awt.geom.AffineTransform", + "fields":[ + {"name":"m00"}, + {"name":"m01"}, + {"name":"m02"}, + {"name":"m10"}, + {"name":"m11"}, + {"name":"m12"} + ] +}, +{ + "name":"java.awt.geom.GeneralPath", + "methods":[ + {"name":"","parameterTypes":[] }, + {"name":"","parameterTypes":["int","byte[]","int","float[]","int"] } + ] +}, +{ + "name":"java.awt.geom.Path2D", + "fields":[ + {"name":"numTypes"}, + {"name":"pointTypes"}, + {"name":"windingRule"} + ] +}, +{ + "name":"java.awt.geom.Path2D$Float", + "fields":[{"name":"floatCoords"}] +}, +{ + "name":"java.awt.geom.Point2D$Float", + "fields":[ + {"name":"x"}, + {"name":"y"} + ], + "methods":[{"name":"","parameterTypes":["float","float"] }] +}, +{ + "name":"java.awt.geom.Rectangle2D$Float", + "fields":[ + {"name":"height"}, + {"name":"width"}, + {"name":"x"}, + {"name":"y"} + ], + "methods":[ + {"name":"","parameterTypes":[] }, + {"name":"","parameterTypes":["float","float","float","float"] } + ] +}, +{ + "name":"java.awt.image.BufferedImage", + "fields":[ + {"name":"colorModel"}, + {"name":"imageType"}, + {"name":"raster"} + ], + "methods":[ + {"name":"getRGB","parameterTypes":["int","int","int","int","int[]","int","int"] }, + {"name":"setRGB","parameterTypes":["int","int","int","int","int[]","int","int"] } + ] +}, +{ + "name":"java.awt.image.ColorModel", + "fields":[ + {"name":"colorSpace"}, + {"name":"colorSpaceType"}, + {"name":"isAlphaPremultiplied"}, + {"name":"is_sRGB"}, + {"name":"nBits"}, + {"name":"numComponents"}, + {"name":"pData"}, + {"name":"supportsAlpha"}, + {"name":"transparency"} + ], + "methods":[{"name":"getRGBdefault","parameterTypes":[] }] +}, +{ + "name":"java.awt.image.DirectColorModel", + "methods":[{"name":"","parameterTypes":["int","int","int","int"] }] +}, +{ + "name":"java.awt.image.IndexColorModel", + "fields":[ + {"name":"allgrayopaque"}, + {"name":"colorData"}, + {"name":"lookupcache"}, + {"name":"map_size"}, + {"name":"rgb"}, + {"name":"transparent_index"} + ] +}, +{ + "name":"java.awt.image.Raster", + "fields":[ + {"name":"dataBuffer"}, + {"name":"height"}, + {"name":"minX"}, + {"name":"minY"}, + {"name":"numBands"}, + {"name":"numDataElements"}, + {"name":"sampleModel"}, + {"name":"sampleModelTranslateX"}, + {"name":"sampleModelTranslateY"}, + {"name":"width"} + ] +}, +{ + "name":"java.awt.image.SampleModel", + "fields":[ + {"name":"height"}, + {"name":"width"} + ], + "methods":[ + {"name":"getPixels","parameterTypes":["int","int","int","int","int[]","java.awt.image.DataBuffer"] }, + {"name":"setPixels","parameterTypes":["int","int","int","int","int[]","java.awt.image.DataBuffer"] } + ] +}, +{ + "name":"java.awt.image.SinglePixelPackedSampleModel", + "fields":[ + {"name":"bitMasks"}, + {"name":"bitOffsets"}, + {"name":"bitSizes"}, + {"name":"maxBitSize"} + ] +}, +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Enum", + "methods":[{"name":"name","parameterTypes":[] }] +}, +{ + "name":"java.lang.Thread", + "methods":[{"name":"currentThread","parameterTypes":[] }] +}, +{ + "name":"java.util.Arrays", + "methods":[{"name":"asList","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"sun.awt.AWTAutoShutdown", + "methods":[ + {"name":"notifyToolkitThreadBusy","parameterTypes":[] }, + {"name":"notifyToolkitThreadFree","parameterTypes":[] } + ] +}, +{ + "name":"sun.awt.EmbeddedFrame" +}, +{ + "name":"sun.awt.ExtendedKeyCodes", + "methods":[{"name":"getExtendedKeyCodeForChar","parameterTypes":["int"] }] +}, +{ + "name":"sun.awt.FontDescriptor", + "fields":[ + {"name":"nativeName"}, + {"name":"useUnicode"} + ] +}, +{ + "name":"sun.awt.LightweightFrame" +}, +{ + "name":"sun.awt.PlatformFont", + "fields":[ + {"name":"componentFonts"}, + {"name":"fontConfig"} + ], + "methods":[{"name":"makeConvertedMultiFontString","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"sun.awt.SunHints", + "fields":[{"name":"INTVAL_STROKE_PURE"}] +}, +{ + "name":"sun.awt.SunToolkit", + "methods":[{"name":"isTouchKeyboardAutoShowEnabled","parameterTypes":[] }] +}, +{ + "name":"sun.awt.Win32GraphicsConfig", + "fields":[{"name":"visual"}] +}, +{ + "name":"sun.awt.Win32GraphicsDevice", + "fields":[{"name":"dynamicColorModel"}] +}, +{ + "name":"sun.awt.Win32GraphicsEnvironment", + "methods":[{"name":"dwmCompositionChanged","parameterTypes":["boolean"] }] +}, +{ + "name":"sun.awt.im.InputMethodWindow" +}, +{ + "name":"sun.awt.image.BufImgSurfaceData$ICMColorData", + "fields":[{"name":"pData"}], + "methods":[{"name":"","parameterTypes":["long"] }] +}, +{ + "name":"sun.awt.image.ByteComponentRaster", + "fields":[ + {"name":"data"}, + {"name":"dataOffsets"}, + {"name":"pixelStride"}, + {"name":"scanlineStride"}, + {"name":"type"} + ] +}, +{ + "name":"sun.awt.image.ImageRepresentation", + "fields":[ + {"name":"numSrcLUT"}, + {"name":"srcLUTtransIndex"} + ] +}, +{ + "name":"sun.awt.image.IntegerComponentRaster", + "fields":[ + {"name":"data"}, + {"name":"dataOffsets"}, + {"name":"pixelStride"}, + {"name":"scanlineStride"}, + {"name":"type"} + ] +}, +{ + "name":"sun.awt.image.SunVolatileImage", + "fields":[{"name":"volSurfaceManager"}] +}, +{ + "name":"sun.awt.image.VolatileSurfaceManager", + "fields":[{"name":"sdCurrent"}] +}, +{ + "name":"sun.awt.windows.WComponentPeer", + "fields":[ + {"name":"hwnd"}, + {"name":"winGraphicsConfig"} + ], + "methods":[ + {"name":"disposeLater","parameterTypes":[] }, + {"name":"postEvent","parameterTypes":["java.awt.AWTEvent"] }, + {"name":"replaceSurfaceData","parameterTypes":[] }, + {"name":"replaceSurfaceDataLater","parameterTypes":[] } + ] +}, +{ + "name":"sun.awt.windows.WDesktopPeer", + "methods":[ + {"name":"systemSleepCallback","parameterTypes":["boolean"] }, + {"name":"userSessionCallback","parameterTypes":["boolean","java.awt.desktop.UserSessionEvent$Reason"] } + ] +}, +{ + "name":"sun.awt.windows.WFontPeer", + "fields":[{"name":"textComponentFontName"}] +}, +{ + "name":"sun.awt.windows.WFramePeer", + "methods":[{"name":"getExtendedState","parameterTypes":[] }] +}, +{ + "name":"sun.awt.windows.WObjectPeer", + "fields":[ + {"name":"createError"}, + {"name":"destroyed"}, + {"name":"pData"}, + {"name":"target"} + ], + "methods":[{"name":"getPeerForTarget","parameterTypes":["java.lang.Object"] }] +}, +{ + "name":"sun.awt.windows.WPanelPeer", + "fields":[{"name":"insets_"}] +}, +{ + "name":"sun.awt.windows.WToolkit", + "methods":[ + {"name":"displayChanged","parameterTypes":[] }, + {"name":"paletteChanged","parameterTypes":[] }, + {"name":"windowsSettingChange","parameterTypes":[] } + ] +}, +{ + "name":"sun.awt.windows.WTrayIconPeer", + "methods":[{"name":"postEvent","parameterTypes":["java.awt.AWTEvent"] }] +}, +{ + "name":"sun.awt.windows.WWindowPeer", + "fields":[{"name":"windowType"}], + "methods":[ + {"name":"draggedToNewScreen","parameterTypes":[] }, + {"name":"notifyWindowStateChanged","parameterTypes":["int","int"] } + ] +}, +{ + "name":"sun.font.CharToGlyphMapper", + "methods":[{"name":"charToGlyph","parameterTypes":["int"] }] +}, +{ + "name":"sun.font.Font2D", + "methods":[ + {"name":"canDisplay","parameterTypes":["char"] }, + {"name":"charToGlyph","parameterTypes":["int"] }, + {"name":"charToVariationGlyph","parameterTypes":["int","int"] }, + {"name":"getMapper","parameterTypes":[] }, + {"name":"getTableBytes","parameterTypes":["int"] } + ] +}, +{ + "name":"sun.font.FontStrike", + "methods":[{"name":"getGlyphMetrics","parameterTypes":["int"] }] +}, +{ + "name":"sun.font.GlyphList", + "fields":[ + {"name":"gposx"}, + {"name":"gposy"}, + {"name":"images"}, + {"name":"lcdRGBOrder"}, + {"name":"lcdSubPixPos"}, + {"name":"len"}, + {"name":"positions"}, + {"name":"usePositions"} + ] +}, +{ + "name":"sun.font.PhysicalStrike", + "fields":[{"name":"pScalerContext"}], + "methods":[ + {"name":"adjustPoint","parameterTypes":["java.awt.geom.Point2D$Float"] }, + {"name":"getGlyphPoint","parameterTypes":["int","int"] } + ] +}, +{ + "name":"sun.font.StrikeMetrics", + "methods":[{"name":"","parameterTypes":["float","float","float","float","float","float","float","float","float","float"] }] +}, +{ + "name":"sun.font.TrueTypeFont", + "methods":[ + {"name":"readBlock","parameterTypes":["java.nio.ByteBuffer","int","int"] }, + {"name":"readBytes","parameterTypes":["int","int"] } + ] +}, +{ + "name":"sun.font.Type1Font", + "methods":[{"name":"readFile","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "name":"sun.java2d.Disposer", + "methods":[{"name":"addRecord","parameterTypes":["java.lang.Object","long","long"] }] +}, +{ + "name":"sun.java2d.InvalidPipeException" +}, +{ + "name":"sun.java2d.NullSurfaceData" +}, +{ + "name":"sun.java2d.SunGraphics2D", + "fields":[ + {"name":"clipRegion"}, + {"name":"composite"}, + {"name":"eargb"}, + {"name":"lcdTextContrast"}, + {"name":"pixel"}, + {"name":"strokeHint"} + ] +}, +{ + "name":"sun.java2d.SurfaceData", + "fields":[ + {"name":"pData"}, + {"name":"valid"} + ] +}, +{ + "name":"sun.java2d.d3d.D3DGraphicsDevice$1", + "methods":[{"name":"run","parameterTypes":[] }] +}, +{ + "name":"sun.java2d.d3d.D3DRenderQueue$1", + "methods":[{"name":"run","parameterTypes":[] }] +}, +{ + "name":"sun.java2d.loops.Blit", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.BlitBg", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.CompositeType", + "fields":[ + {"name":"AnyAlpha"}, + {"name":"Src"}, + {"name":"SrcNoEa"}, + {"name":"SrcOver"}, + {"name":"SrcOverNoEa"}, + {"name":"Xor"} + ] +}, +{ + "name":"sun.java2d.loops.DrawGlyphList", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.DrawGlyphListAA", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.DrawGlyphListLCD", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.DrawLine", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.DrawParallelogram", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.DrawPath", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.DrawPolygons", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.DrawRect", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.FillParallelogram", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.FillPath", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.FillRect", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.FillSpans", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.GraphicsPrimitive", + "fields":[{"name":"pNativePrim"}] +}, +{ + "name":"sun.java2d.loops.GraphicsPrimitiveMgr", + "methods":[{"name":"register","parameterTypes":["sun.java2d.loops.GraphicsPrimitive[]"] }] +}, +{ + "name":"sun.java2d.loops.MaskBlit", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.MaskFill", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.ScaledBlit", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.SurfaceType", + "fields":[ + {"name":"Any3Byte"}, + {"name":"Any4Byte"}, + {"name":"AnyByte"}, + {"name":"AnyColor"}, + {"name":"AnyInt"}, + {"name":"AnyShort"}, + {"name":"ByteBinary1Bit"}, + {"name":"ByteBinary2Bit"}, + {"name":"ByteBinary4Bit"}, + {"name":"ByteGray"}, + {"name":"ByteIndexed"}, + {"name":"ByteIndexedBm"}, + {"name":"FourByteAbgr"}, + {"name":"FourByteAbgrPre"}, + {"name":"Index12Gray"}, + {"name":"Index8Gray"}, + {"name":"IntArgb"}, + {"name":"IntArgbBm"}, + {"name":"IntArgbPre"}, + {"name":"IntBgr"}, + {"name":"IntRgb"}, + {"name":"IntRgbx"}, + {"name":"OpaqueColor"}, + {"name":"ThreeByteBgr"}, + {"name":"Ushort4444Argb"}, + {"name":"Ushort555Rgb"}, + {"name":"Ushort555Rgbx"}, + {"name":"Ushort565Rgb"}, + {"name":"UshortGray"}, + {"name":"UshortIndexed"} + ] +}, +{ + "name":"sun.java2d.loops.TransformHelper", + "methods":[{"name":"","parameterTypes":["long","sun.java2d.loops.SurfaceType","sun.java2d.loops.CompositeType","sun.java2d.loops.SurfaceType"] }] +}, +{ + "name":"sun.java2d.loops.XORComposite", + "fields":[ + {"name":"alphaMask"}, + {"name":"xorColor"}, + {"name":"xorPixel"} + ] +}, +{ + "name":"sun.java2d.pipe.Region", + "fields":[ + {"name":"bands"}, + {"name":"endIndex"}, + {"name":"hix"}, + {"name":"hiy"}, + {"name":"lox"}, + {"name":"loy"} + ] +}, +{ + "name":"sun.java2d.pipe.RegionIterator", + "fields":[ + {"name":"curIndex"}, + {"name":"numXbands"}, + {"name":"region"} + ] +}, +{ + "name":"sun.java2d.windows.WindowsFlags", + "fields":[ + {"name":"d3dEnabled"}, + {"name":"d3dSet"}, + {"name":"offscreenSharingEnabled"}, + {"name":"setHighDPIAware"} + ] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[ + {"name":"compTimeMonitoringSupport"}, + {"name":"currentThreadCpuTimeSupport"}, + {"name":"objectMonitorUsageSupport"}, + {"name":"otherThreadCpuTimeSupport"}, + {"name":"remoteDiagnosticCommandsSupport"}, + {"name":"synchronizerUsageSupport"}, + {"name":"threadAllocatedMemorySupport"}, + {"name":"threadContentionMonitoringSupport"} + ] +} +] diff --git a/core/src/main/resources/META-INF/native-image/predefined-classes-config.json b/core/src/main/resources/META-INF/native-image/predefined-classes-config.json new file mode 100644 index 000000000..0e79b2c5d --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/predefined-classes-config.json @@ -0,0 +1,8 @@ +[ + { + "type":"agent-extracted", + "classes":[ + ] + } +] + diff --git a/core/src/main/resources/META-INF/native-image/proxy-config.json b/core/src/main/resources/META-INF/native-image/proxy-config.json new file mode 100644 index 000000000..6097e9bdf --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/proxy-config.json @@ -0,0 +1,158 @@ +[ + { + "interfaces":["com.fasterxml.jackson.annotation.JsonProperty","org.glassfish.jaxb.core.v2.model.annotation.Locatable"] + }, + { + "interfaces":["jakarta.xml.bind.annotation.XmlAccessorType","org.glassfish.jaxb.core.v2.model.annotation.Locatable"] + }, + { + "interfaces":["jakarta.xml.bind.annotation.XmlSeeAlso","org.glassfish.jaxb.core.v2.model.annotation.Locatable"] + }, + { + "interfaces":["jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter","org.glassfish.jaxb.core.v2.model.annotation.Locatable"] + }, + { + "interfaces":["java.lang.reflect.ParameterizedType","org.springframework.core.SerializableTypeWrapper$SerializableTypeProxy","java.io.Serializable"] + }, + { + "interfaces":["java.lang.reflect.TypeVariable","org.springframework.core.SerializableTypeWrapper$SerializableTypeProxy","java.io.Serializable"] + }, + { + "interfaces":["java.lang.reflect.WildcardType","org.springframework.core.SerializableTypeWrapper$SerializableTypeProxy","java.io.Serializable"] + }, + { + "interfaces":["java.sql.Connection"] + }, + { + "interfaces":["net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$Executable"] + }, + { + "interfaces":["net.bytebuddy.description.method.ParameterDescription$ForLoadedParameter$Parameter"] + }, + { + "interfaces":["net.bytebuddy.description.method.ParameterList$ForLoadedExecutable$Executable"] + }, + { + "interfaces":["net.bytebuddy.description.type.TypeDefinition$Sort$AnnotatedType"] + }, + { + "interfaces":["net.bytebuddy.description.type.TypeDescription$ForLoadedType$Dispatcher"] + }, + { + "interfaces":["net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$Delegator$ForLoadedExecutableExceptionType$Dispatcher"] + }, + { + "interfaces":["net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$Delegator$ForLoadedExecutableParameterType$Dispatcher"] + }, + { + "interfaces":["net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$Delegator$ForLoadedMethodReturnType$Dispatcher"] + }, + { + "interfaces":["net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$ForComponentType$AnnotatedParameterizedType"] + }, + { + "interfaces":["net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$ForTypeArgument$AnnotatedParameterizedType"] + }, + { + "interfaces":["net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles"] + }, + { + "interfaces":["net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles$Lookup"] + }, + { + "interfaces":["org.hibernate.Session","org.springframework.orm.jpa.EntityManagerProxy"] + }, + { + "interfaces":["org.hibernate.SessionFactory","org.springframework.orm.jpa.EntityManagerFactoryInfo"] + }, + { + "interfaces":["org.hibernate.query.hql.spi.SqmQueryImplementor","org.hibernate.query.sqm.internal.SqmInterpretationsKey$InterpretationsKeySource","org.hibernate.query.spi.DomainQueryExecutionContext","org.hibernate.query.SelectionQuery","org.hibernate.query.CommonQueryContract"] + }, + { + "interfaces":["org.hibernate.query.sql.spi.NativeQueryImplementor","org.hibernate.query.spi.DomainQueryExecutionContext","org.hibernate.query.internal.ResultSetMappingResolutionContext","org.hibernate.query.spi.QueryImplementor","org.hibernate.query.SelectionQuery","org.hibernate.query.CommonQueryContract"] + }, + { + "interfaces":["org.nzbhydra.downloading.FileDownloadRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.indexers.IndexerApiAccessEntityShortRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.indexers.IndexerApiAccessRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.indexers.IndexerRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.indexers.IndexerSearchRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.indexers.status.IndexerLimitRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.mediainfo.MovieInfoRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.mediainfo.TvInfoRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.news.ShownNewsRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.notifications.NotificationRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.searching.db.SearchRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.searching.db.SearchResultRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.nzbhydra.searching.uniqueness.IndexerUniquenessScoreEntityRepository","org.springframework.data.repository.Repository","org.springframework.transaction.interceptor.TransactionalProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.springframework.beans.factory.annotation.Qualifier"] + }, + { + "interfaces":["org.springframework.boot.actuate.endpoint.annotation.Endpoint"] + }, + { + "interfaces":["org.springframework.boot.actuate.endpoint.annotation.EndpointExtension"] + }, + { + "interfaces":["org.springframework.boot.context.properties.ConfigurationProperties"] + }, + { + "interfaces":["org.springframework.cache.annotation.Cacheable"] + }, + { + "interfaces":["org.springframework.context.event.EventListener"] + }, + { + "interfaces":["org.springframework.data.jpa.repository.support.CrudMethodMetadata","org.springframework.aop.SpringProxy","org.springframework.aop.framework.Advised","org.springframework.core.DecoratingProxy"] + }, + { + "interfaces":["org.springframework.jdbc.datasource.ConnectionProxy"] + }, + { + "interfaces":["org.springframework.web.bind.annotation.ControllerAdvice"] + }, + { + "interfaces":["org.springframework.web.bind.annotation.CrossOrigin"] + }, + { + "interfaces":["org.springframework.web.bind.annotation.PathVariable"] + }, + { + "interfaces":["org.springframework.web.bind.annotation.RequestMapping"] + }, + { + "interfaces":["org.springframework.web.bind.annotation.RequestParam"] + }, + { + "interfaces":["org.springframework.web.bind.annotation.ResponseStatus"] + }, + { + "interfaces":["org.mockito.plugins.PluginSwitch"] + } +] diff --git a/core/src/main/resources/META-INF/native-image/reflect-config.json b/core/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000..ca29583bf --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,36304 @@ +[ +{ + "name":"[B" +}, +{ + "name":"[C" +}, +{ + "name":"[D" +}, +{ + "name":"[F" +}, +{ + "name":"[I" +}, +{ + "name":"[J" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.Deserializers;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.KeyDeserializers;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.ValueInstantiators;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.ser.Serializers;" +}, +{ + "name":"[Ljava.io.Serializable;" +}, +{ + "name":"[Ljava.lang.CharSequence;" +}, +{ + "name":"[Ljava.lang.Class;" +}, +{ + "name":"[Ljava.lang.Comparable;" +}, +{ + "name":"[Ljava.lang.Object;" +}, +{ + "name":"[Ljava.lang.String;" +}, +{ + "name":"[Ljava.lang.annotation.Annotation;" +}, +{ + "name":"[Ljava.lang.constant.Constable;" +}, +{ + "name":"[Ljava.lang.constant.ConstantDesc;" +}, +{ + "name":"[Ljava.sql.Statement;" +}, +{ + "name":"[Ljavax.management.openmbean.CompositeData;" +}, +{ + "name":"[Lorg.hibernate.event.spi.AutoFlushEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.DeleteEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.DirtyCheckEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.EvictEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.FlushEntityEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.FlushEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.InitializeCollectionEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.LoadEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.LockEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.MergeEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PersistEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PostDeleteEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PostInsertEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PostLoadEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PostUpdateEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PreDeleteEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PreInsertEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PreLoadEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.PreUpdateEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.RefreshEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.ReplicateEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.ResolveNaturalIdEventListener;" +}, +{ + "name":"[Lorg.hibernate.event.spi.SaveOrUpdateEventListener;" +}, +{ + "name":"[Lorg.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;" +}, +{ + "name":"[Lorg.springframework.boot.context.config.ConfigDataLocation;" +}, +{ + "name":"[Lorg.springframework.core.annotation.AnnotationAttributes;" +}, +{ + "name":"[Lorg.springframework.core.annotation.TypeMappedAnnotation;" +}, +{ + "name":"[Lorg.springframework.core.io.InputStreamSource;" +}, +{ + "name":"[Lorg.springframework.core.io.Resource;", + "queryAllDeclaredMethods":true +}, +{ + "name":"[Lorg.springframework.util.ConcurrentReferenceHashMap$Segment;" +}, +{ + "name":"[Lorg.springframework.web.bind.annotation.RequestMethod;" +}, +{ + "name":"[Lsun.security.pkcs.SignerInfo;" +}, +{ + "name":"[S" +}, +{ + "name":"[Z" +}, +{ + "name":"boolean", + "queryAllDeclaredMethods":true +}, +{ + "name":"brave.Tracer" +}, +{ + "name":"ch.qos.logback.classic.LoggerContext" +}, +{ + "name":"ch.qos.logback.classic.filter.ThresholdFilter", + "queryAllPublicMethods":true, + "methods":[ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setLevel", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"ch.qos.logback.classic.pattern.DateConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.pattern.LevelConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.pattern.LineSeparatorConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.pattern.LoggerConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.pattern.MarkerConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.pattern.MessageConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.pattern.ThreadConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.ConsoleAppender", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.OutputStreamAppender", + "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] +}, +{ + "name":"ch.qos.logback.core.UnsynchronizedAppenderBase", + "methods":[{"name":"addFilter","parameterTypes":["ch.qos.logback.core.filter.Filter"] }] +}, +{ + "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] +}, +{ + "name":"ch.qos.logback.core.filter.Filter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "decide", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "isStarted", + "parameterTypes": [] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "start", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [] + } + ] +}, +{ + "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.pattern.ReplacingCompositeConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.rolling.RollingFileAppender", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setFile", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setRollingPolicy", + "parameterTypes": [ + "ch.qos.logback.core.rolling.RollingPolicy" + ] + } + ] +}, +{ + "name":"ch.qos.logback.core.rolling.RollingPolicyBase", + "methods": [ + { + "name": "setFileNamePattern", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setParent", + "parameterTypes": [ + "ch.qos.logback.core.FileAppender" + ] + } + ] +}, +{ + "name":"ch.qos.logback.core.rolling.TimeBasedRollingPolicy", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setMaxHistory", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"ch.qos.logback.core.rolling.helper.DateTokenConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.spi.ContextAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"ch.qos.logback.core.spi.ContextAwareBase", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addError", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "addError", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] + }, + { + "name": "addInfo", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "addInfo", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] + }, + { + "name": "addStatus", + "parameterTypes": [ + "ch.qos.logback.core.status.Status" + ] + }, + { + "name": "addWarn", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "addWarn", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] + }, + { + "name": "getContext", + "parameterTypes": [] + }, + { + "name": "getStatusManager", + "parameterTypes": [] + }, + { + "name": "setContext", + "parameterTypes": [ + "ch.qos.logback.core.Context" + ] + } + ] +}, +{ + "name":"ch.qos.logback.core.spi.LifeCycle", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"char", + "queryAllDeclaredMethods":true +}, +{ + "name":"co.elastic.clients.elasticsearch.ElasticsearchClient" +}, +{ + "name":"co.elastic.clients.transport.ElasticsearchTransport" +}, +{ + "name":"com.couchbase.client.java.Bucket" +}, +{ + "name":"com.couchbase.client.java.Cluster" +}, +{ + "name":"com.datastax.oss.driver.api.core.CqlSession" +}, +{ + "name":"com.fasterxml.jackson.annotation.JacksonAnnotation", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonFormat", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonFormat$Shape" +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonIgnore", + "queryAllDeclaredMethods":true, + "methods":[{"name":"value","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonIgnoreProperties", + "queryAllDeclaredMethods":true, + "methods":[{"name":"value","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonInclude", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonInclude$Include" +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonProperty", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonPropertyOrder", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.fasterxml.jackson.annotation.JsonSetter", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.fasterxml.jackson.core.JsonGenerator" +}, +{ + "name":"com.fasterxml.jackson.core.ObjectCodec", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getJsonFactory", + "parameterTypes": [] + }, + { + "name": "readValues", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.core.type.ResolvedType" + ] + }, + { + "name": "readValues", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.core.type.TypeReference" + ] + } + ] +}, +{ + "name":"com.fasterxml.jackson.core.TreeCodec", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, + { + "name": "com.fasterxml.jackson.core.Versioned", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true + }, + { + "name": "com.fasterxml.jackson.databind.DeserializationFeature", + "queryAllDeclaredMethods": true + }, + { + "name": "com.fasterxml.jackson.databind.JsonDeserializer", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "deserialize", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.databind.DeserializationContext", + "java.lang.Object" + ] + }, + { + "name": "deserializeWithType", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.databind.DeserializationContext", + "com.fasterxml.jackson.databind.jsontype.TypeDeserializer" + ] + }, + { + "name": "deserializeWithType", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.databind.DeserializationContext", + "com.fasterxml.jackson.databind.jsontype.TypeDeserializer", + "java.lang.Object" + ] + }, + { + "name": "findBackReference", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getAbsentValue", + "parameterTypes": [ + "com.fasterxml.jackson.databind.DeserializationContext" + ] + }, + { + "name": "getDelegatee", + "parameterTypes": [] + }, + { + "name": "getEmptyAccessPattern", + "parameterTypes": [] + }, + { + "name": "getEmptyValue", + "parameterTypes": [] + }, + { + "name": "getEmptyValue", + "parameterTypes": [ + "com.fasterxml.jackson.databind.DeserializationContext" + ] + }, + { + "name": "getKnownPropertyNames", + "parameterTypes": [] + }, + { + "name": "getNullAccessPattern", + "parameterTypes": [] + }, + { + "name": "getNullValue", + "parameterTypes": [] + }, + { + "name": "getNullValue", + "parameterTypes": [ + "com.fasterxml.jackson.databind.DeserializationContext" + ] + }, + { + "name": "getObjectIdReader", + "parameterTypes": [] + }, + { + "name": "handledType", + "parameterTypes": [] + }, + { + "name": "isCachable", + "parameterTypes": [] + }, + { + "name": "logicalType", + "parameterTypes": [] + }, + { + "name": "replaceDelegatee", + "parameterTypes": [ + "com.fasterxml.jackson.databind.JsonDeserializer" + ] + }, + { + "name": "supportsUpdate", + "parameterTypes": [ + "com.fasterxml.jackson.databind.DeserializationConfig" + ] + }, + { + "name": "unwrappingDeserializer", + "parameterTypes": [ + "com.fasterxml.jackson.databind.util.NameTransformer" + ] + } + ] + }, + { + "name": "com.fasterxml.jackson.databind.JsonSerializer", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "acceptJsonFormatVisitor", + "parameterTypes": [ + "com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper", + "com.fasterxml.jackson.databind.JavaType" + ] + }, + { + "name": "getDelegatee", + "parameterTypes": [] + }, + { + "name": "handledType", + "parameterTypes": [] + }, + { + "name": "isEmpty", + "parameterTypes": [ + "com.fasterxml.jackson.databind.SerializerProvider", + "java.lang.Object" + ] + }, + { + "name": "isEmpty", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "isUnwrappingSerializer", + "parameterTypes": [] + }, + { + "name": "properties", + "parameterTypes": [] + }, + { + "name": "replaceDelegatee", + "parameterTypes": [ + "com.fasterxml.jackson.databind.JsonSerializer" + ] + }, + { + "name": "serialize", + "parameterTypes": [ + "java.lang.Object", + "com.fasterxml.jackson.core.JsonGenerator", + "com.fasterxml.jackson.databind.SerializerProvider" + ] + }, + { + "name": "serializeWithType", + "parameterTypes": [ + "java.lang.Object", + "com.fasterxml.jackson.core.JsonGenerator", + "com.fasterxml.jackson.databind.SerializerProvider", + "com.fasterxml.jackson.databind.jsontype.TypeSerializer" + ] + }, + { + "name": "unwrappingSerializer", + "parameterTypes": [ + "com.fasterxml.jackson.databind.util.NameTransformer" + ] + }, + { + "name": "usesObjectId", + "parameterTypes": [] + }, + { + "name": "withFilterId", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"com.fasterxml.jackson.databind.Module", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getDependencies","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.databind.ObjectMapper", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "createArrayNode", + "parameterTypes": [] + }, + { + "name": "createObjectNode", + "parameterTypes": [] + }, + { + "name": "getFactory", + "parameterTypes": [] + }, + { + "name": "missingNode", + "parameterTypes": [] + }, + { + "name": "nullNode", + "parameterTypes": [] + }, + { + "name": "readTree", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser" + ] + }, + { + "name": "readValue", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.core.type.ResolvedType" + ] + }, + { + "name": "readValue", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.core.type.TypeReference" + ] + }, + { + "name": "readValue", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "java.lang.Class" + ] + }, + { + "name": "readValues", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.core.type.ResolvedType" + ] + }, + { + "name": "readValues", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.core.type.TypeReference" + ] + }, + { + "name": "readValues", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "java.lang.Class" + ] + }, + { + "name": "treeAsTokens", + "parameterTypes": [ + "com.fasterxml.jackson.core.TreeNode" + ] + }, + { + "name": "treeToValue", + "parameterTypes": [ + "com.fasterxml.jackson.core.TreeNode", + "java.lang.Class" + ] + }, + { + "name": "version", + "parameterTypes": [] + }, + { + "name": "writeTree", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonGenerator", + "com.fasterxml.jackson.core.TreeNode" + ] + }, + { + "name": "writeValue", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonGenerator", + "java.lang.Object" + ] + } + ] +}, +{ + "name":"com.fasterxml.jackson.databind.SerializationFeature", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.fasterxml.jackson.databind.annotation.JacksonStdImpl", + "queryAllDeclaredMethods":true +}, + { + "name": "com.fasterxml.jackson.databind.annotation.JsonDeserialize", + "queryAllDeclaredMethods": true + }, + { + "name": "com.fasterxml.jackson.databind.annotation.JsonSerialize", + "queryAllDeclaredMethods": true + }, + { + "name": "com.fasterxml.jackson.databind.deser.NullValueProvider", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true + }, + { + "name": "com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods":true +}, +{ + "name":"com.fasterxml.jackson.databind.module.SimpleModule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addAbstractTypeMapping", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Class" + ] + }, + { + "name": "addDeserializer", + "parameterTypes": [ + "java.lang.Class", + "com.fasterxml.jackson.databind.JsonDeserializer" + ] + }, + { + "name": "addKeyDeserializer", + "parameterTypes": [ + "java.lang.Class", + "com.fasterxml.jackson.databind.KeyDeserializer" + ] + }, + { + "name": "addKeySerializer", + "parameterTypes": [ + "java.lang.Class", + "com.fasterxml.jackson.databind.JsonSerializer" + ] + }, + { + "name": "addSerializer", + "parameterTypes": [ + "com.fasterxml.jackson.databind.JsonSerializer" + ] + }, + { + "name": "addSerializer", + "parameterTypes": [ + "java.lang.Class", + "com.fasterxml.jackson.databind.JsonSerializer" + ] + }, + { + "name": "addValueInstantiator", + "parameterTypes": [ + "java.lang.Class", + "com.fasterxml.jackson.databind.deser.ValueInstantiator" + ] + }, + { + "name": "getModuleName", + "parameterTypes": [] + }, + { + "name": "getTypeId", + "parameterTypes": [] + }, + { + "name": "registerSubtypes", + "parameterTypes": [ + "java.util.Collection" + ] + }, + { + "name": "registerSubtypes", + "parameterTypes": [ + "com.fasterxml.jackson.databind.jsontype.NamedType[]" + ] + }, + { + "name": "registerSubtypes", + "parameterTypes": [ + "java.lang.Class[]" + ] + }, + { + "name": "setAbstractTypes", + "parameterTypes": [ + "com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver" + ] + }, + { + "name": "setDeserializerModifier", + "parameterTypes": [ + "com.fasterxml.jackson.databind.deser.BeanDeserializerModifier" + ] + }, + { + "name": "setDeserializers", + "parameterTypes": [ + "com.fasterxml.jackson.databind.module.SimpleDeserializers" + ] + }, + { + "name": "setKeyDeserializers", + "parameterTypes": [ + "com.fasterxml.jackson.databind.module.SimpleKeyDeserializers" + ] + }, + { + "name": "setKeySerializers", + "parameterTypes": [ + "com.fasterxml.jackson.databind.module.SimpleSerializers" + ] + }, + { + "name": "setMixInAnnotation", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Class" + ] + }, + { + "name": "setSerializerModifier", + "parameterTypes": [ + "com.fasterxml.jackson.databind.ser.BeanSerializerModifier" + ] + }, + { + "name": "setSerializers", + "parameterTypes": [ + "com.fasterxml.jackson.databind.module.SimpleSerializers" + ] + }, + { + "name": "setValueInstantiators", + "parameterTypes": [ + "com.fasterxml.jackson.databind.module.SimpleValueInstantiators" + ] + }, + { + "name": "setupModule", + "parameterTypes": [ + "com.fasterxml.jackson.databind.Module$SetupContext" + ] + }, + { + "name": "version", + "parameterTypes": [] + } + ] +}, +{ + "name":"com.fasterxml.jackson.dataformat.cbor$CBORFactory" +}, +{ + "name":"com.fasterxml.jackson.dataformat.cbor.CBORFactory" +}, +{ + "name":"com.fasterxml.jackson.dataformat.smile$SmileFactory" +}, +{ + "name":"com.fasterxml.jackson.dataformat.smile.SmileFactory" +}, +{ + "name":"com.fasterxml.jackson.dataformat.xml$XmlMapper" +}, +{ + "name":"com.fasterxml.jackson.dataformat.xml.XmlMapper" +}, +{ + "name":"com.fasterxml.jackson.datatype.jdk8.Jdk8Module", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.module.kotlin$KotlinModule" +}, +{ + "name":"com.fasterxml.jackson.module.kotlin.KotlinModule" +}, +{ + "name":"com.fasterxml.jackson.module.paramnames.ParameterNamesModule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "setupModule", + "parameterTypes": [ + "com.fasterxml.jackson.databind.Module$SetupContext" + ] + } + ] +}, +{ + "name":"com.gargoylesoftware.htmlunit.BrowserVersionFeatures", + "fields": [ + { + "name": "ANCHOR_EMPTY_HREF_NO_FILENAME" + }, + { + "name": "ANCHOR_SEND_PING_REQUEST" + }, + { + "name": "CONTENT_SECURITY_POLICY_IGNORED" + }, + { + "name": "CSS_BACKGROUND_INITIAL" + }, + { + "name": "CSS_BACKGROUND_RGBA" + }, + { + "name": "CSS_CSSTEXT_IE_STYLE" + }, + { + "name": "CSS_DIALOG_NONE" + }, + { + "name": "CSS_DISPLAY_BLOCK" + }, + { + "name": "CSS_DISPLAY_BLOCK2" + }, + { + "name": "CSS_LENGTH_INITIAL" + }, + { + "name": "CSS_NOSCRIPT_DISPLAY_INLINE" + }, + { + "name": "CSS_OUTLINE_WIDTH_UNIT_NOT_REQUIRED" + }, + { + "name": "CSS_PROGRESS_DISPLAY_INLINE" + }, + { + "name": "CSS_PSEUDO_SELECTOR_MS_PLACEHHOLDER" + }, + { + "name": "CSS_PSEUDO_SELECTOR_PLACEHOLDER_SHOWN" + }, + { + "name": "CSS_RP_DISPLAY_NONE" + }, + { + "name": "CSS_RT_DISPLAY_RUBY_TEXT_ALWAYS" + }, + { + "name": "CSS_RUBY_DISPLAY_INLINE" + }, + { + "name": "CSS_SET_NULL_THROWS" + }, + { + "name": "CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY" + }, + { + "name": "CSS_STYLE_PROP_FONT_DISCONNECTED_IS_EMPTY" + }, + { + "name": "CSS_VERTICAL_ALIGN_SUPPORTS_AUTO" + }, + { + "name": "CSS_ZINDEX_TYPE_INTEGER" + }, + { + "name": "DIALOGWINDOW_REFERER" + }, + { + "name": "DOM_NORMALIZE_REMOVE_CHILDREN" + }, + { + "name": "EVENT_BEFORE_UNLOAD_RETURN_VALUE_IS_HTML5_LIKE" + }, + { + "name": "EVENT_FOCUS_FOCUS_IN_BLUR_OUT" + }, + { + "name": "EVENT_FOCUS_IN_FOCUS_OUT_BLUR" + }, + { + "name": "EVENT_FOCUS_ON_LOAD" + }, + { + "name": "EVENT_HANDLER_NULL_RETURN_IS_MEANINGFUL" + }, + { + "name": "EVENT_MOUSE_ON_DISABLED" + }, + { + "name": "EVENT_ONANIMATION_DOCUMENT_CREATE_NOT_SUPPORTED" + }, + { + "name": "EVENT_ONCHANGE_AFTER_ONCLICK" + }, + { + "name": "EVENT_ONCLICK_FOR_SELECT_ONLY" + }, + { + "name": "EVENT_ONCLICK_POINTEREVENT_DETAIL_0" + }, + { + "name": "EVENT_ONCLICK_USES_POINTEREVENT" + }, + { + "name": "EVENT_ONCLOSE_DOCUMENT_CREATE_NOT_SUPPORTED" + }, + { + "name": "EVENT_ONDOUBLECLICK_USES_POINTEREVENT" + }, + { + "name": "EVENT_ONLOAD_INTERNAL_JAVASCRIPT" + }, + { + "name": "EVENT_ONMESSAGE_DEFAULT_DATA_NULL" + }, + { + "name": "EVENT_ONMOUSEDOWN_FOR_SELECT_OPTION_TRIGGERS_ADDITIONAL_DOWN_FOR_SELECT" + }, + { + "name": "EVENT_ONMOUSEDOWN_NOT_FOR_SELECT_OPTION" + }, + { + "name": "EVENT_ONMOUSEOVER_FOR_DISABLED_OPTION" + }, + { + "name": "EVENT_ONMOUSEOVER_NEVER_FOR_SELECT_OPTION" + }, + { + "name": "EVENT_ONMOUSEUP_FOR_SELECT_OPTION_TRIGGERS_ADDITIONAL_UP_FOR_SELECT" + }, + { + "name": "EVENT_ONMOUSEUP_NOT_FOR_SELECT_OPTION" + }, + { + "name": "EVENT_ONPOPSTATE_DOCUMENT_CREATE_NOT_SUPPORTED" + }, + { + "name": "EVENT_TYPE_BEFOREUNLOADEVENT" + }, + { + "name": "EVENT_TYPE_HASHCHANGEEVENT" + }, + { + "name": "EVENT_TYPE_MOUSEWHEELEVENT" + }, + { + "name": "EVENT_TYPE_POINTEREVENT" + }, + { + "name": "EVENT_TYPE_PROGRESSEVENT" + }, + { + "name": "EVENT_TYPE_WHEELEVENT" + }, + { + "name": "FOCUS_BODY_ELEMENT_AT_START" + }, + { + "name": "FORMFIELD_REACHABLE_BY_NEW_NAMES" + }, + { + "name": "FORMFIELD_REACHABLE_BY_ORIGINAL_NAME" + }, + { + "name": "FORM_FORM_ATTRIBUTE_SUPPORTED" + }, + { + "name": "FORM_PARAMETRS_NOT_SUPPORTED_FOR_IMAGE" + }, + { + "name": "FORM_SUBMISSION_DOWNLOWDS_ALSO_IF_ONLY_HASH_CHANGED" + }, + { + "name": "FORM_SUBMISSION_FORM_ATTRIBUTE" + }, + { + "name": "FORM_SUBMISSION_HEADER_CACHE_CONTROL_MAX_AGE" + }, + { + "name": "FORM_SUBMISSION_HEADER_CACHE_CONTROL_NO_CACHE" + }, + { + "name": "FORM_SUBMISSION_HEADER_ORIGIN" + }, + { + "name": "FORM_SUBMISSION_URL_WITHOUT_HASH" + }, + { + "name": "FRAME_LOCATION_ABOUT_BLANK_FOR_ABOUT_SCHEME" + }, + { + "name": "HTMLABBREVIATED" + }, + { + "name": "HTMLALLCOLLECTION_DO_NOT_CONVERT_STRINGS_TO_NUMBER" + }, + { + "name": "HTMLALLCOLLECTION_INTEGER_INDEX" + }, + { + "name": "HTMLALLCOLLECTION_NO_COLLECTION_FOR_MANY_HITS" + }, + { + "name": "HTMLALLCOLLECTION_NULL_IF_NAMED_ITEM_NOT_FOUND" + }, + { + "name": "HTMLBASEFONT_END_TAG_FORBIDDEN" + }, + { + "name": "HTMLBASE_HREF_DEFAULT_EMPTY" + }, + { + "name": "HTMLBUTTON_SUBMIT_IGNORES_DISABLED_STATE" + }, + { + "name": "HTMLBUTTON_WILL_VALIDATE_IGNORES_READONLY" + }, + { + "name": "HTMLCOLLECTION_ITEM_FUNCT_SUPPORTS_DOUBLE_INDEX_ALSO" + }, + { + "name": "HTMLCOLLECTION_ITEM_SUPPORTS_DOUBLE_INDEX_ALSO" + }, + { + "name": "HTMLCOLLECTION_ITEM_SUPPORTS_ID_SEARCH_ALSO" + }, + { + "name": "HTMLCOLLECTION_NAMED_ITEM_ID_FIRST" + }, + { + "name": "HTMLCOLLECTION_NULL_IF_NOT_FOUND" + }, + { + "name": "HTMLCOLLECTION_SUPPORTS_PARANTHESES" + }, + { + "name": "HTMLDOCUMENT_CHARSET_LOWERCASE" + }, + { + "name": "HTMLDOCUMENT_COLOR" + }, + { + "name": "HTMLDOCUMENT_COOKIES_IGNORE_BLANK" + }, + { + "name": "HTMLDOCUMENT_ELEMENTS_BY_NAME_EMPTY" + }, + { + "name": "HTMLDOCUMENT_FUNCTION_DETACHED" + }, + { + "name": "HTMLDOCUMENT_GET_ALSO_FRAMES" + }, + { + "name": "HTMLDOCUMENT_GET_FOR_ID_AND_OR_NAME" + }, + { + "name": "HTMLDOCUMENT_GET_PREFERS_STANDARD_FUNCTIONS" + }, + { + "name": "HTMLELEMENT_ALIGN_INVALID" + }, + { + "name": "HTMLELEMENT_DETACH_ACTIVE_TRIGGERS_NO_KEYUP_EVENT" + }, + { + "name": "HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT" + }, + { + "name": "HTMLIMAGE_BLANK_SRC_AS_EMPTY" + }, + { + "name": "HTMLIMAGE_EMPTY_SRC_DISPLAY_FALSE" + }, + { + "name": "HTMLIMAGE_HTMLELEMENT" + }, + { + "name": "HTMLIMAGE_HTMLUNKNOWNELEMENT" + }, + { + "name": "HTMLIMAGE_INVISIBLE_NO_SRC" + }, + { + "name": "HTMLIMAGE_NAME_VALUE_PARAMS" + }, + { + "name": "HTMLINPUT_ATTRIBUTE_MIN_MAX_LENGTH_SUPPORTED" + }, + { + "name": "HTMLINPUT_CHECKBOX_DOES_NOT_CLICK_SURROUNDING_ANCHOR" + }, + { + "name": "HTMLINPUT_DOES_NOT_CLICK_SURROUNDING_ANCHOR" + }, + { + "name": "HTMLINPUT_FILES_UNDEFINED" + }, + { + "name": "HTMLINPUT_FILE_SELECTION_START_END_NULL" + }, + { + "name": "HTMLINPUT_TYPE_COLOR_NOT_SUPPORTED" + }, + { + "name": "HTMLINPUT_TYPE_DATETIME_LOCAL_SUPPORTED" + }, + { + "name": "HTMLINPUT_TYPE_DATETIME_SUPPORTED" + }, + { + "name": "HTMLINPUT_TYPE_IMAGE_IGNORES_CUSTOM_VALIDITY" + }, + { + "name": "HTMLINPUT_TYPE_MONTH_SUPPORTED" + }, + { + "name": "HTMLINPUT_TYPE_WEEK_SUPPORTED" + }, + { + "name": "HTMLKEYGEN_END_TAG_FORBIDDEN" + }, + { + "name": "HTMLLINK_CHECK_TYPE_FOR_STYLESHEET" + }, + { + "name": "HTMLOPTION_PREVENT_DISABLED" + }, + { + "name": "HTMLOPTION_REMOVE_SELECTED_ATTRIB_DESELECTS" + }, + { + "name": "HTMLSCRIPT_TRIM_TYPE" + }, + { + "name": "HTMLSELECT_WILL_VALIDATE_ALWAYS_TRUE" + }, + { + "name": "HTMLSELECT_WILL_VALIDATE_IGNORES_READONLY" + }, + { + "name": "HTMLTEXTAREA_SET_DEFAULT_VALUE_UPDATES_VALUE" + }, + { + "name": "HTMLTEXTAREA_USE_ALL_TEXT_CHILDREN" + }, + { + "name": "HTMLTEXTAREA_WILL_VALIDATE_IGNORES_READONLY" + }, + { + "name": "HTMLTRACK_END_TAG_FORBIDDEN" + }, + { + "name": "HTML_ATTRIBUTE_LOWER_CASE" + }, + { + "name": "HTML_COLOR_EXPAND_ZERO" + }, + { + "name": "HTML_COLOR_RESTRICT" + }, + { + "name": "HTML_COLOR_TO_LOWER" + }, + { + "name": "HTML_COMMAND_TAG" + }, + { + "name": "HTML_ISINDEX_TAG" + }, + { + "name": "HTML_MAIN_TAG" + }, + { + "name": "HTML_OBJECT_CLASSID" + }, + { + "name": "HTTP_COOKIE_EXTENDED_DATE_PATTERNS_1" + }, + { + "name": "HTTP_COOKIE_EXTENDED_DATE_PATTERNS_2" + }, + { + "name": "HTTP_COOKIE_EXTRACT_PATH_FROM_LOCATION" + }, + { + "name": "HTTP_COOKIE_REMOVE_DOT_FROM_ROOT_DOMAINS" + }, + { + "name": "HTTP_COOKIE_START_DATE_1970" + }, + { + "name": "HTTP_HEADER_CH_UA" + }, + { + "name": "HTTP_HEADER_SEC_FETCH" + }, + { + "name": "HTTP_HEADER_UPGRADE_INSECURE_REQUEST" + }, + { + "name": "HTTP_REDIRECT_WITHOUT_HASH" + }, + { + "name": "JS_ALIGN_ACCEPTS_ARBITRARY_VALUES" + }, + { + "name": "JS_ALIGN_FOR_INPUT_IGNORES_VALUES" + }, + { + "name": "JS_ANCHORS_REQUIRES_NAME_OR_ID" + }, + { + "name": "JS_ANCHOR_HOSTNAME_IGNORE_BLANK" + }, + { + "name": "JS_ANCHOR_PATHNAME_DETECT_WIN_DRIVES_URL" + }, + { + "name": "JS_ANCHOR_PATHNAME_NONE_FOR_BROKEN_URL" + }, + { + "name": "JS_ANCHOR_PATHNAME_NONE_FOR_NONE_HTTP_URL" + }, + { + "name": "JS_ANCHOR_PATHNAME_PREFIX_WIN_DRIVES_URL" + }, + { + "name": "JS_ANCHOR_PROTOCOL_COLON_FOR_BROKEN_URL" + }, + { + "name": "JS_ANCHOR_PROTOCOL_COLON_UPPER_CASE_DRIVE_LETTERS" + }, + { + "name": "JS_ANCHOR_PROTOCOL_HTTP_FOR_BROKEN_URL" + }, + { + "name": "JS_ANCHOR_PROTOCOL_INVALID_THROWS" + }, + { + "name": "JS_API_FETCH" + }, + { + "name": "JS_API_PROXY" + }, + { + "name": "JS_AREA_WITHOUT_HREF_FOCUSABLE" + }, + { + "name": "JS_ARGUMENTS_READ_ONLY_ACCESSED_FROM_FUNCTION" + }, + { + "name": "JS_ARRAY_FROM" + }, + { + "name": "JS_ATTR_FIRST_LAST_CHILD_RETURNS_NULL" + }, + { + "name": "JS_AUDIO_PROCESSING_EVENT_CTOR" + }, + { + "name": "JS_BGSOUND_AS_UNKNOWN" + }, + { + "name": "JS_BLOB_CONTENT_TYPE_CASE_SENSITIVE" + }, + { + "name": "JS_BLOB_EVENT_REQUIRES_DATA" + }, + { + "name": "JS_BODY_MARGINS_8" + }, + { + "name": "JS_BOUNDINGCLIENTRECT_THROWS_IF_DISCONNECTED" + }, + { + "name": "JS_CANVAS_DATA_URL_CHROME_PNG" + }, + { + "name": "JS_CANVAS_DATA_URL_IE_PNG" + }, + { + "name": "JS_CLEAR_RESTRICT" + }, + { + "name": "JS_CLIENTHEIGHT_INPUT_17" + }, + { + "name": "JS_CLIENTHEIGHT_INPUT_18" + }, + { + "name": "JS_CLIENTHEIGHT_RADIO_CHECKBOX_10" + }, + { + "name": "JS_CLIENTRECTLIST_THROWS_IF_ITEM_NOT_FOUND" + }, + { + "name": "JS_CLIENTWIDTH_INPUT_TEXT_143" + }, + { + "name": "JS_CLIENTWIDTH_INPUT_TEXT_173" + }, + { + "name": "JS_CLIENTWIDTH_RADIO_CHECKBOX_10" + }, + { + "name": "JS_CONSOLE_TIMESTAMP" + }, + { + "name": "JS_CSSRULELIST_ENUM_ITEM_LENGTH" + }, + { + "name": "JS_CSS_OBJECT" + }, + { + "name": "JS_DATE_LOCALE_DATE_SHORT" + }, + { + "name": "JS_DATE_WITH_LEFT_TO_RIGHT_MARK" + }, + { + "name": "JS_DOCTYPE_ENTITIES_NULL" + }, + { + "name": "JS_DOCTYPE_NOTATIONS_NULL" + }, + { + "name": "JS_DOCUMENT_CREATE_ATTRUBUTE_LOWER_CASE" + }, + { + "name": "JS_DOCUMENT_DESIGN_MODE_INHERIT" + }, + { + "name": "JS_DOCUMENT_FORMS_FUNCTION_SUPPORTED" + }, + { + "name": "JS_DOCUMENT_OPEN_OVERWRITES_ABOUT_BLANK_LOCATION" + }, + { + "name": "JS_DOCUMENT_SELECTION_RANGE_COUNT" + }, + { + "name": "JS_DOCUMENT_SETTING_DOMAIN_THROWS_FOR_ABOUT_BLANK" + }, + { + "name": "JS_DOMIMPLEMENTATION_CREATE_HTMLDOCOMENT_REQUIRES_TITLE" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CORE_3" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS2_1" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS2_3" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS3_1" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS3_2" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS3_3" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS_1" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS_2" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_CSS_3" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_EVENTS_1" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_KEYBOARDEVENTS" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_LS" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_MUTATIONNAMEEVENTS" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_RANGE_1" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_RANGE_3" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_STYLESHEETS" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_SVG_BASICSTRUCTURE_1_2" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_TEXTEVENTS" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_UIEVENTS_2" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_VALIDATION" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_VIEWS_1" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_VIEWS_3" + }, + { + "name": "JS_DOMIMPLEMENTATION_FEATURE_XPATH" + }, + { + "name": "JS_DOMPARSER_EMPTY_STRING_IS_ERROR" + }, + { + "name": "JS_DOMPARSER_EXCEPTION_ON_ERROR" + }, + { + "name": "JS_DOMPARSER_PARSERERROR_ON_ERROR" + }, + { + "name": "JS_DOMTOKENLIST_CONTAINS_RETURNS_FALSE_FOR_BLANK" + }, + { + "name": "JS_DOMTOKENLIST_ENHANCED_WHITESPACE_CHARS" + }, + { + "name": "JS_DOMTOKENLIST_GET_NULL_IF_OUTSIDE" + }, + { + "name": "JS_DOMTOKENLIST_LENGTH_IGNORES_DUPLICATES" + }, + { + "name": "JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_ADD" + }, + { + "name": "JS_DOMTOKENLIST_REMOVE_WHITESPACE_CHARS_ON_REMOVE" + }, + { + "name": "JS_DOM_CDATA_DELETE_THROWS_NEGATIVE_COUNT" + }, + { + "name": "JS_ELEMENT_GET_ATTRIBUTE_RETURNS_EMPTY" + }, + { + "name": "JS_ERROR_CAPTURE_STACK_TRACE" + }, + { + "name": "JS_ERROR_STACK_TRACE_LIMIT" + }, + { + "name": "JS_EVENT_INPUT_CTOR_INPUTTYPE" + }, + { + "name": "JS_EVENT_KEYBOARD_CTOR_WHICH" + }, + { + "name": "JS_EVENT_LOAD_SUPPRESSED_BY_CONTENT_SECURIRY_POLICY" + }, + { + "name": "JS_FILEREADER_CONTENT_TYPE" + }, + { + "name": "JS_FILEREADER_EMPTY_NULL" + }, + { + "name": "JS_FORM_ACTION_EXPANDURL_NOT_DEFINED" + }, + { + "name": "JS_FORM_DATA_CONTENT_TYPE_PLAIN_IF_FILE_TYPE_UNKNOWN" + }, + { + "name": "JS_FORM_DATA_ITERATOR_SIMPLE_NAME" + }, + { + "name": "JS_FORM_DISPATCHEVENT_SUBMITS" + }, + { + "name": "JS_FORM_REJECT_INVALID_ENCODING" + }, + { + "name": "JS_FORM_SUBMIT_FORCES_DOWNLOAD" + }, + { + "name": "JS_FORM_USABLE_AS_FUNCTION" + }, + { + "name": "JS_FRAME_CONTENT_DOCUMENT_ACCESS_DENIED_THROWS" + }, + { + "name": "JS_GLOBAL_THIS" + }, + { + "name": "JS_GROUPINGRULE_INSERTRULE_INDEX_OPTIONAL" + }, + { + "name": "JS_HTML_HYPHEN_ELEMENT_CLASS_NAME" + }, + { + "name": "JS_HTML_OBJECT_VALIDITYSTATE_ISVALID_IGNORES_CUSTOM_ERROR" + }, + { + "name": "JS_HTML_RUBY_ELEMENT_CLASS_NAME" + }, + { + "name": "JS_IFRAME_ALWAYS_EXECUTE_ONLOAD" + }, + { + "name": "JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED" + }, + { + "name": "JS_IGNORES_UTF8_BOM_SOMETIMES" + }, + { + "name": "JS_IMAGE_COMPLETE_RETURNS_TRUE_FOR_NO_REQUEST" + }, + { + "name": "JS_IMAGE_WIDTH_HEIGHT_EMPTY_SOURCE_RETURNS_0x0" + }, + { + "name": "JS_IMAGE_WIDTH_HEIGHT_RETURNS_16x16_0x0" + }, + { + "name": "JS_IMAGE_WIDTH_HEIGHT_RETURNS_24x24_0x0" + }, + { + "name": "JS_IMAGE_WIDTH_HEIGHT_RETURNS_28x30_28x30" + }, + { + "name": "JS_INNER_HTML_ADD_CHILD_FOR_NULL_VALUE" + }, + { + "name": "JS_INNER_HTML_LF" + }, + { + "name": "JS_INNER_TEXT_SCRIPT" + }, + { + "name": "JS_INNER_TEXT_SVG_NL" + }, + { + "name": "JS_INNER_TEXT_SVG_TITLE" + }, + { + "name": "JS_INNER_TEXT_VALUE_NULL" + }, + { + "name": "JS_INPUT_IGNORE_NEGATIVE_SELECTION_START" + }, + { + "name": "JS_INPUT_NUMBER_ACCEPT_ALL" + }, + { + "name": "JS_INPUT_NUMBER_DOT_AT_END_IS_DOUBLE" + }, + { + "name": "JS_INPUT_NUMBER_SELECTION_START_END_NULL" + }, + { + "name": "JS_INPUT_SET_TYPE_LOWERCASE" + }, + { + "name": "JS_INPUT_SET_UNSUPORTED_TYPE_EXCEPTION" + }, + { + "name": "JS_INPUT_SET_VALUE_DATE_SUPPORTED" + }, + { + "name": "JS_INPUT_SET_VALUE_EMAIL_TRIMMED" + }, + { + "name": "JS_INPUT_SET_VALUE_MOVE_SELECTION_TO_START" + }, + { + "name": "JS_INPUT_SET_VALUE_URL_TRIMMED" + }, + { + "name": "JS_INTL_NAMED_OBJECT" + }, + { + "name": "JS_INTL_V8_BREAK_ITERATOR" + }, + { + "name": "JS_IS_SEARCH_PROVIDER_INSTALLED_ZERO" + }, + { + "name": "JS_LABEL_FORM_OF_SELF" + }, + { + "name": "JS_LOCATION_HASH_HASH_IS_ENCODED" + }, + { + "name": "JS_LOCATION_HASH_IS_DECODED" + }, + { + "name": "JS_LOCATION_HASH_RETURNS_HASH_FOR_EMPTY_DEFINED" + }, + { + "name": "JS_LOCATION_HREF_HASH_IS_ENCODED" + }, + { + "name": "JS_LOCATION_RELOAD_REFERRER" + }, + { + "name": "JS_MEDIA_LIST_ALL" + }, + { + "name": "JS_MEDIA_LIST_EMPTY_STRING" + }, + { + "name": "JS_MENU_TYPE_EMPTY" + }, + { + "name": "JS_MENU_TYPE_PASS" + }, + { + "name": "JS_NATIVE_FUNCTION_TOSTRING_COMPACT" + }, + { + "name": "JS_NATIVE_FUNCTION_TOSTRING_NEW_LINE" + }, + { + "name": "JS_NATIVE_FUNCTION_TOSTRING_NL" + }, + { + "name": "JS_NAVIGATOR_DO_NOT_TRACK_UNSPECIFIED" + }, + { + "name": "JS_NODE_CONTAINS_RETURNS_FALSE_FOR_INVALID_ARG" + }, + { + "name": "JS_NODE_INSERT_BEFORE_REF_OPTIONAL" + }, + { + "name": "JS_OBJECT_GET_OWN_PROPERTY_SYMBOLS" + }, + { + "name": "JS_OFFSET_PARENT_NULL_IF_FIXED" + }, + { + "name": "JS_OUTER_HTML_NULL_AS_STRING" + }, + { + "name": "JS_OUTER_HTML_REMOVES_CHILDREN_FOR_DETACHED" + }, + { + "name": "JS_OUTER_HTML_THROWS_FOR_DETACHED" + }, + { + "name": "JS_PAGERULE_SELECTORTEXT_EMPTY" + }, + { + "name": "JS_PHRASE_COMMON_CLASS_NAME" + }, + { + "name": "JS_POP_STATE_EVENT_CLONE_STATE" + }, + { + "name": "JS_PRE_WIDTH_STRING" + }, + { + "name": "JS_PROMISE" + }, + { + "name": "JS_PROPERTY_DESCRIPTOR_NAME" + }, + { + "name": "JS_PROPERTY_DESCRIPTOR_NEW_LINE" + }, + { + "name": "JS_REFLECT" + }, + { + "name": "JS_REGEXP_EMPTY_LASTPAREN_IF_TOO_MANY_GROUPS" + }, + { + "name": "JS_REGEXP_GROUP0_RETURNS_WHOLE_MATCH" + }, + { + "name": "JS_SCRIPT_HANDLE_204_AS_ERROR" + }, + { + "name": "JS_SCRIPT_SUPPORTS_FOR_AND_EVENT_WINDOW" + }, + { + "name": "JS_SELECTOR_TEXT_LOWERCASE" + }, + { + "name": "JS_SELECT_FILE_THROWS" + }, + { + "name": "JS_SELECT_OPTIONS_HAS_SELECT_CLASS_NAME" + }, + { + "name": "JS_SELECT_OPTIONS_IGNORE_NEGATIVE_LENGTH" + }, + { + "name": "JS_SELECT_OPTIONS_IN_ALWAYS_TRUE" + }, + { + "name": "JS_SELECT_OPTIONS_NULL_FOR_OUTSIDE" + }, + { + "name": "JS_SELECT_OPTIONS_REMOVE_IGNORE_IF_INDEX_NEGATIVE" + }, + { + "name": "JS_SELECT_OPTIONS_REMOVE_THROWS_IF_NEGATIV" + }, + { + "name": "JS_SELECT_REMOVE_IGNORE_IF_INDEX_OUTSIDE" + }, + { + "name": "JS_SELECT_SET_VALUES_CHECKS_ONLY_VALUE_ATTRIBUTE" + }, + { + "name": "JS_STORAGE_GET_FROM_ITEMS" + }, + { + "name": "JS_STORAGE_PRESERVED_INCLUDED" + }, + { + "name": "JS_STYLESHEETLIST_ACTIVE_ONLY" + }, + { + "name": "JS_STYLE_UNSUPPORTED_PROPERTY_GETTER" + }, + { + "name": "JS_STYLE_WORD_SPACING_ACCEPTS_PERCENT" + }, + { + "name": "JS_STYLE_WRONG_INDEX_RETURNS_UNDEFINED" + }, + { + "name": "JS_SYMBOL" + }, + { + "name": "JS_TABLE_CELL_HEIGHT_DOES_NOT_RETURN_NEGATIVE_VALUES" + }, + { + "name": "JS_TABLE_CELL_OFFSET_INCLUDES_BORDER" + }, + { + "name": "JS_TABLE_CELL_WIDTH_DOES_NOT_RETURN_NEGATIVE_VALUES" + }, + { + "name": "JS_TABLE_COLUMN_WIDTH_NO_NEGATIVE_VALUES" + }, + { + "name": "JS_TABLE_COLUMN_WIDTH_NULL_STRING" + }, + { + "name": "JS_TABLE_ROW_DELETE_CELL_REQUIRES_INDEX" + }, + { + "name": "JS_TABLE_SPAN_SET_ZERO_IF_INVALID" + }, + { + "name": "JS_TABLE_SPAN_THROWS_EXCEPTION_IF_INVALID" + }, + { + "name": "JS_TABLE_VALIGN_SUPPORTS_IE_VALUES" + }, + { + "name": "JS_TEXT_AREA_GET_MAXLENGTH_MAX_INT" + }, + { + "name": "JS_TEXT_AREA_SET_COLS_NEGATIVE_THROWS_EXCEPTION" + }, + { + "name": "JS_TEXT_AREA_SET_COLS_THROWS_EXCEPTION" + }, + { + "name": "JS_TEXT_AREA_SET_MAXLENGTH_NEGATIVE_THROWS_EXCEPTION" + }, + { + "name": "JS_TEXT_AREA_SET_ROWS_NEGATIVE_THROWS_EXCEPTION" + }, + { + "name": "JS_TEXT_AREA_SET_ROWS_THROWS_EXCEPTION" + }, + { + "name": "JS_TEXT_AREA_SET_VALUE_NULL" + }, + { + "name": "JS_TREEWALKER_EXPAND_ENTITY_REFERENCES_FALSE" + }, + { + "name": "JS_TREEWALKER_FILTER_FUNCTION_ONLY" + }, + { + "name": "JS_TYPE_ACCEPTS_ARBITRARY_VALUES" + }, + { + "name": "JS_URL_SEARCH_PARMS_ITERATOR_SIMPLE_NAME" + }, + { + "name": "JS_VALIGN_CONVERTS_TO_LOWERCASE" + }, + { + "name": "JS_WEBGL_CONTEXT_EVENT_CONSTANTS" + }, + { + "name": "JS_WIDTH_HEIGHT_ACCEPTS_ARBITRARY_VALUES" + }, + { + "name": "JS_WINDOW_ACTIVEXOBJECT_HIDDEN" + }, + { + "name": "JS_WINDOW_CHANGE_OPENER_ONLY_WINDOW_OBJECT" + }, + { + "name": "JS_WINDOW_COMPUTED_STYLE_PSEUDO_ACCEPT_WITHOUT_COLON" + }, + { + "name": "JS_WINDOW_FORMFIELDS_ACCESSIBLE_BY_NAME" + }, + { + "name": "JS_WINDOW_FRAMES_ACCESSIBLE_BY_ID" + }, + { + "name": "JS_WINDOW_FRAME_BY_ID_RETURNS_WINDOW" + }, + { + "name": "JS_WINDOW_INSTALL_TRIGGER_NULL" + }, + { + "name": "JS_WINDOW_OUTER_INNER_HEIGHT_DIFF_131" + }, + { + "name": "JS_WINDOW_OUTER_INNER_HEIGHT_DIFF_133" + }, + { + "name": "JS_WINDOW_OUTER_INNER_HEIGHT_DIFF_86" + }, + { + "name": "JS_WINDOW_OUTER_INNER_HEIGHT_DIFF_91" + }, + { + "name": "JS_WINDOW_SELECTION_NULL_IF_INVISIBLE" + }, + { + "name": "JS_WINDOW_TOP_WRITABLE" + }, + { + "name": "JS_WORKER_IMPORT_SCRIPTS_ACCEPTS_ALL" + }, + { + "name": "JS_XML" + }, + { + "name": "JS_XML_GET_ELEMENTS_BY_TAG_NAME_LOCAL" + }, + { + "name": "JS_XML_GET_ELEMENT_BY_ID__ANY_ELEMENT" + }, + { + "name": "JS_XML_SERIALIZER_BLANK_BEFORE_SELF_CLOSING" + }, + { + "name": "JS_XML_SERIALIZER_HTML_DOCUMENT_FRAGMENT_ALWAYS_EMPTY" + }, + { + "name": "JS_XML_SERIALIZER_ROOT_CDATA_AS_ESCAPED_TEXT" + }, + { + "name": "JS_XML_SUPPORT_VIA_ACTIVEXOBJECT" + }, + { + "name": "JS_XSLT_TRANSFORM_INDENT" + }, + { + "name": "KEYBOARD_EVENT_SPECIAL_KEYPRESS" + }, + { + "name": "KEYGEN_AS_BLOCK" + }, + { + "name": "META_X_UA_COMPATIBLE" + }, + { + "name": "MULTICOL_BLOCK" + }, + { + "name": "PAGE_SELECTION_RANGE_FROM_SELECTABLE_TEXT_INPUT" + }, + { + "name": "QUERYSELECTORALL_NOT_IN_QUIRKS" + }, + { + "name": "QUERYSELECTOR_CSS3_PSEUDO_REQUIRE_ATTACHED_NODE" + }, + { + "name": "RESETINPUT_DEFAULT_VALUE_IF_VALUE_NOT_DEFINED" + }, + { + "name": "SLOT_CONTENTS" + }, + { + "name": "STRING_INCLUDES" + }, + { + "name": "STRING_REPEAT" + }, + { + "name": "STRING_STARTS_ENDS_WITH" + }, + { + "name": "STRING_TRIM_LEFT_RIGHT" + }, + { + "name": "STYLESHEET_ADD_RULE_RETURNS_POS" + }, + { + "name": "STYLESHEET_HREF_EMPTY_IS_NULL" + }, + { + "name": "SUBMITINPUT_DEFAULT_VALUE_IF_VALUE_NOT_DEFINED" + }, + { + "name": "SVG_UNKNOWN_ARE_DOM" + }, + { + "name": "URL_ABOUT_BLANK_HAS_BLANK_PATH" + }, + { + "name": "URL_AUTH_CREDENTIALS" + }, + { + "name": "URL_MINIMAL_QUERY_ENCODING" + }, + { + "name": "URL_MISSING_SLASHES" + }, + { + "name": "WEBSOCKET_ORIGIN_SET" + }, + { + "name": "WINDOW_EXECUTE_EVENTS" + }, + { + "name": "XHR_ALL_RESPONSE_HEADERS_APPEND_SEPARATOR" + }, + { + "name": "XHR_ALL_RESPONSE_HEADERS_SEPARATE_BY_LF" + }, + { + "name": "XHR_FIRE_STATE_OPENED_AGAIN_IN_ASYNC_MODE" + }, + { + "name": "XHR_HANDLE_SYNC_NETWORK_ERRORS" + }, + { + "name": "XHR_LENGTH_COMPUTABLE" + }, + { + "name": "XHR_LOAD_ALWAYS_AFTER_DONE" + }, + { + "name": "XHR_LOAD_START_ASYNC" + }, + { + "name": "XHR_NO_CROSS_ORIGIN_TO_ABOUT" + }, + { + "name": "XHR_OPEN_ALLOW_EMTPY_URL" + }, + { + "name": "XHR_PROGRESS_ON_NETWORK_ERROR_ASYNC" + }, + { + "name": "XHR_RESPONSE_TEXT_EMPTY_UNSENT" + }, + { + "name": "XHR_RESPONSE_TYPE_THROWS_UNSENT" + }, + { + "name": "XHR_SEND_IGNORES_BLOB_MIMETYPE_AS_CONTENTTYPE" + }, + { + "name": "XHR_SEND_NETWORK_ERROR_IF_ABORTED" + }, + { + "name": "XHR_USE_CONTENT_CHARSET" + }, + { + "name": "XPATH_SELECTION_NAMESPACES" + } + ] +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.ApplicationCache", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.AudioScheduledSourceNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.BarProp", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.BatteryManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.BroadcastChannel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Cache", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.CacheStorage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.ClientRect", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.ClientRectList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Element", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.External", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.FontFace", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.FontFaceSet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Gamepad", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.GamepadButton", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.History", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.ImageBitmap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Location", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.MessageChannel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.MessagePort", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.MimeType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.MimeTypeArray", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.NamedNodeMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Navigator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Notification", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.PerformanceObserver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.PerformanceObserverEntryList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.PermissionStatus", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Permissions", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Plugin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.PluginArray", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.PushManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.PushSubscription", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.PushSubscriptionOptions", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.ReadableStream", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Screen", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.ScreenOrientation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.SharedWorker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.SimpleArray", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Storage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.StorageManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.TextDecoder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.TextEncoder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Touch", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.TouchList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.URL", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.URLSearchParams", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.WebSocket", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.Window", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.XPathExpression", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.animations.Animation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.animations.AnimationEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.animations.KeyframeEffect", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.arrays.Atomics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.CanvasCaptureMediaStreamTrack", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.CanvasGradient", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.CanvasPattern", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.CanvasRenderingContext2D", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.ImageBitmapRenderingContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.ImageData", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.IntersectionObserver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.IntersectionObserverEntry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.Path2D", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.TextMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGL2RenderingContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLActiveInfo", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLBuffer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLFramebuffer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLProgram", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLQuery", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLRenderbuffer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLRenderingContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLSampler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLShader", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLShaderPrecisionFormat", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLSync", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLTexture", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLTransformFeedback", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLUniformLocation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.canvas.WebGLVertexArrayObject", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.crypto.Crypto", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.crypto.CryptoKey", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.crypto.SubtleCrypto", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSS", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSConditionRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSCounterStyleRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSFontFaceRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSGroupingRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSImportRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSKeyframeRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSKeyframesRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSMediaRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSNamespaceRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSPageRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleDeclaration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.CSSSupportsRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.MediaList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.MediaQueryList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.StyleMedia", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.StyleSheet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.StyleSheetList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.css.WebKitCSSMatrix", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.AbstractList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.Attr", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.CDATASection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.CharacterData", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.Comment", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.CustomElementRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMError", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMException", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMImplementation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMMatrix", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMMatrixReadOnly", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMParser", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMPoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMPointReadOnly", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMRectReadOnly", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMStringList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMStringMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DOMTokenList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.Document", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DocumentFragment", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.DocumentType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.IdleDeadline", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.MutationObserver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.MutationRecord", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.Node", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.NodeFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.NodeIterator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.NodeList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.ProcessingInstruction", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.RadioNodeList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.Range", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.Selection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.ShadowRoot", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.Text", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.TreeWalker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.WebKitMutationObserver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.XPathEvaluator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.XPathNSResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.dom.XPathResult", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.AudioProcessingEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.BeforeInstallPromptEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.BeforeUnloadEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.BlobEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.ClipboardEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.CloseEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.CompositionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.CustomEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.DeviceMotionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.DeviceOrientationEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.DragEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.ErrorEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.Event", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.EventSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.FocusEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.GamepadEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.HashChangeEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.IDBVersionChangeEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.InputEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.KeyboardEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MIDIConnectionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MIDIMessageEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MediaEncryptedEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MediaKeyMessageEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MediaQueryListEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MediaStreamEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MediaStreamTrackEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MessageEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MouseEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.MutationEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.OfflineAudioCompletionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.PageTransitionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.PointerEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.PopStateEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.PresentationConnectionAvailableEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.PresentationConnectionCloseEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.ProgressEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.PromiseRejectionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.RTCDataChannelEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.RTCPeerConnectionIceEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.SecurityPolicyViolationEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.SpeechSynthesisEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.StorageEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.TextEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.TouchEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.TrackEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.TransitionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.UIEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.WebGLContextEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.WebkitSpeechRecognitionError", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.WebkitSpeechRecognitionEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.event.WheelEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.fetch.Headers", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.fetch.Request", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.fetch.Response", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.file.Blob", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.file.DataTransferItem", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.file.DataTransferItemList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.file.File", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.file.FileList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.file.FileReader", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.geo.Geolocation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.Audio", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.DataTransfer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLAllCollection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLAnchorElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLAreaElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLAudioElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLBGSoundElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLBRElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLBaseElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLBlockElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLBodyElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLButtonElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLCanvasElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLCollection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDListElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDataElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDataListElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDetailsElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDialogElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDirectoryElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDivElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLEmbedElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLFieldSetElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLFontElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLFormControlsCollection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLFormElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLFrameElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLFrameSetElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLHRElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLHeadElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLHeadingElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLHtmlElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLIFrameElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLImageElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLInputElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLLIElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLLabelElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLLegendElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLLinkElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLListElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLMapElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLMarqueeElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLMediaElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLMenuElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLMetaElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLMeterElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLModElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLOListElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLObjectElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLOptGroupElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLOptionElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLOptionsCollection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLOutputElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLParagraphElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLParamElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLPhraseElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLPictureElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLPreElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLProgressElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLQuoteElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLScriptElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLSelectElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLSlotElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLSourceElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLSpanElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLStyleElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableCaptionElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableCellElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableColElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableComponent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableRowElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableSectionElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTemplateElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTextAreaElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTimeElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTitleElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTrackElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLUListElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLUnknownElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.HTMLVideoElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.Image", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.Option", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.RowContainer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.html.ValidityState", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBCursor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBCursorWithValue", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBDatabase", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBFactory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBIndex", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBKeyRange", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBObjectStore", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBOpenDBRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.idb.IDBTransaction", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AnalyserNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AudioBuffer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AudioBufferSourceNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AudioContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AudioDestinationNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AudioListener", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AudioNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.AudioParam", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.BaseAudioContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.BiquadFilterNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.ChannelMergerNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.ChannelSplitterNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.ConstantSourceNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.ConvolverNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.DelayNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.DynamicsCompressorNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.GainNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.IIRFilterNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.InputDeviceCapabilities", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaDeviceInfo", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaDevices", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaElementAudioSourceNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaError", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaKeySession", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaKeyStatusMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaKeySystemAccess", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaKeys", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaRecorder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaStream", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaStreamAudioDestinationNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaStreamAudioSourceNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.MediaStreamTrack", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.OfflineAudioContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.OscillatorNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.PannerNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.PeriodicSyncManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.PeriodicWave", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.RemotePlayback", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.ScriptProcessorNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.SourceBuffer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.SourceBufferList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.StereoPannerNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.TextTrack", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.TextTrackCue", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.TextTrackCueList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.TextTrackList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.TimeRanges", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.VTTCue", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.VideoPlaybackQuality", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.WaveShaperNode", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.WebkitMediaStream", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.midi.MIDIAccess", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.midi.MIDIInput", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.midi.MIDIInputMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.midi.MIDIOutput", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.midi.MIDIOutputMap", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.midi.MIDIPort", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.presentation.Presentation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.presentation.PresentationAvailability", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.presentation.PresentationConnection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.presentation.PresentationRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.rtc.RTCCertificate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.rtc.RTCIceCandidate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.rtc.RTCPeerConnection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.rtc.RTCSessionDescription", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.rtc.RTCStatsReport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.media.rtc.WebkitRTCPeerConnection", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.network.NetworkInformation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.payment.PaymentAddress", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.payment.PaymentRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.payment.PaymentResponse", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.Performance", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.PerformanceEntry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.PerformanceMark", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.PerformanceMeasure", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.PerformanceNavigation", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.PerformanceNavigationTiming", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.PerformanceResourceTiming", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.performance.PerformanceTiming", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.security.Credential", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.security.CredentialsContainer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.security.FederatedCredential", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.security.PasswordCredential", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.speech.SpeechSynthesis", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.speech.SpeechSynthesisErrorEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.speech.SpeechSynthesisUtterance", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.speech.WebkitSpeechGrammar", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.speech.WebkitSpeechGrammarList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.speech.WebkitSpeechRecognition", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAngle", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimateElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimateMotionElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimateTransformElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedAngle", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedBoolean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedEnumeration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedInteger", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedLength", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedLengthList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedNumber", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedNumberList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedPreserveAspectRatio", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedRect", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedString", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimatedTransformList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGAnimationElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGCircleElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGClipPathElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGComponentTransferFunctionElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGDefsElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGDescElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGEllipseElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEBlendElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEColorMatrixElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEComponentTransferElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFECompositeElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEConvolveMatrixElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEDiffuseLightingElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEDisplacementMapElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEDistantLightElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEDropShadowElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEFloodElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEFuncAElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEFuncBElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEFuncGElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEFuncRElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEGaussianBlurElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEImageElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEMergeElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEMergeNodeElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEMorphologyElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEOffsetElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFEPointLightElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFESpecularLightingElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFESpotLightElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFETileElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFETurbulenceElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGFilterElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGForeignObjectElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGGElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGGeometryElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGGradientElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGGraphicsElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGImageElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGLength", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGLengthList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGLineElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGLinearGradientElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGMPathElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGMarkerElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGMaskElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGMatrix", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGMetadataElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGNumber", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGNumberList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGPathElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGPatternElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGPoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGPointList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGPolygonElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGPolylineElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGPreserveAspectRatio", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGRadialGradientElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGRect", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGRectElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGSVGElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGScriptElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGSetElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGStopElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGStringList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGStyleElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGSwitchElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGSymbolElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTSpanElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTextContentElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTextElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTextPathElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTextPositioningElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTitleElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTransform", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGTransformList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGUnitTypes", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGUseElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.svg.SVGViewElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.webkitURL", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.worker.ServiceWorker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.worker.ServiceWorkerContainer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.worker.ServiceWorkerRegistration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.worker.SyncManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.worker.Worker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.xml.FormData", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.xml.XMLDocument", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.xml.XMLHttpRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.xml.XMLHttpRequestEventTarget", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.xml.XMLHttpRequestUpload", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.xml.XMLSerializer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.javascript.host.xml.XSLTProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAltGlyph", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAltGlyphDef", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAltGlyphItem", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAnchor", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAnimate", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAnimateColor", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAnimateMotion", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgAnimateTransform", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgCircle", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgClipPath", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgColorProfile", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgCursor", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgDefs", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgDesc", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgEllipse", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeBlend", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeColorMatrix", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeComponentTransfer", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeComposite", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeConvolveMatrix", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeDiffuseLighting", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeDisplacementMap", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeDistantLight", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeFlood", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeFuncA", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeFuncB", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeFuncG", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeFuncR", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeGaussianBlur", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeImage", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeMerge", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeMergeNode", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeMorphology", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeOffset", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFePointLight", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeSpecularLighting", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeSpotLight", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeTile", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFeTurbulence", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFilter", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFont", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFontFace", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFontFaceFormat", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFontFaceName", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFontFaceSrc", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgFontFaceURI", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgForeignObject", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgGlyph", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgGlyphRef", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgGroup", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgHKern", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgImage", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgLine", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgLinearGradient", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgMPath", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgMarker", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgMask", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgMetadata", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgMissingGlyph", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgPath", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgPattern", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgPolygon", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgPolyline", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgRadialGradient", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgRect", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgScript", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgSet", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgStop", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgStyle", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgSwitch", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgSymbol", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgTRef", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgTSpan", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgText", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgTextPath", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgTitle", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgUse", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgVKern", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.gargoylesoftware.htmlunit.svg.SvgView", + "fields":[{"name":"TAG_NAME"}] +}, +{ + "name":"com.github.benmanes.caffeine.cache.Cache" +}, +{ + "name":"com.github.benmanes.caffeine.cache.UnboundedLocalCache", + "fields":[{"name":"refreshes"}] +}, +{ + "name": "com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect" +}, + { + "name": "com.google.common.base.Optional" + }, + { + "name": "com.google.common.cache.ElementTypesAreNonnullByDefault", + "queryAllDeclaredMethods": true + }, + { + "name": "com.google.common.collect.Multimap" + }, + { + "name": "com.google.errorprone.annotations.CheckReturnValue", + "queryAllDeclaredMethods": true + }, + { + "name": "com.google.gson.Gson", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "methods": [ + { + "name": "toString", + "parameterTypes": [] + } + ] + }, +{ + "name":"com.google.gson.GsonBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.hazelcast.core.Hazelcast" +}, +{ + "name":"com.hazelcast.core.HazelcastInstance" +}, +{ + "name":"com.hazelcast.spring.cache.HazelcastCache" +}, +{ + "name":"com.ibm.icu.text.Collator" +}, +{ + "name":"com.ibm.lang.management.OperatingSystemMXBean" +}, +{ + "name":"com.ibm.websphere.wsoc$WsWsocServerContainer" +}, +{ + "name":"com.ibm.websphere.wsoc.WsWsocServerContainer" +}, +{ + "name":"com.mongodb.MongoClientSettings" +}, +{ + "name":"com.mongodb.client.MongoClient" +}, +{ + "name":"com.mongodb.reactivestreams.client.MongoClient" +}, +{ + "name":"com.querydsl.core.types$Predicate" +}, +{ + "name":"com.querydsl.core.types.Predicate" +}, +{ + "name":"com.rabbitmq.client.Channel" +}, +{ + "name":"com.rabbitmq.client.ConnectionFactory" +}, +{ + "name":"com.rometools.rome.feed$WireFeed" +}, +{ + "name":"com.rometools.rome.feed.WireFeed" +}, +{ + "name":"com.samskivert.mustache$Template" +}, +{ + "name":"com.samskivert.mustache.Mustache" +}, +{ + "name":"com.samskivert.mustache.Template" +}, +{ + "name":"com.sendgrid.SendGrid" +}, +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DHParameters", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.PBKDF2Core$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.TlsKeyMaterialGenerator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.TlsMasterSecretGenerator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.TlsPrfGenerator$V12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.management.GarbageCollectionNotificationInfo" +}, +{ + "name":"com.sun.management.GarbageCollectorMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.GcInfo", + "queryAllPublicMethods":true, + "fields": [ + { + "name": "builder" + }, + { + "name": "extAttributes" + } + ], + "methods":[{"name":"getMemoryUsageBeforeGc","parameterTypes":[] }] +}, +{ + "name":"com.sun.management.HotSpotDiagnosticMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.OperatingSystemMXBean", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getCpuLoad", + "parameterTypes": [] + }, + { + "name": "getProcessCpuLoad", + "parameterTypes": [] + } + ] +}, +{ + "name":"com.sun.management.ThreadMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.UnixOperatingSystemMXBean" +}, +{ + "name":"com.sun.management.VMOption", + "queryAllPublicMethods":true +}, +{ + "name":"com.sun.management.internal.GarbageCollectorExtImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.management.internal.HotSpotDiagnostic", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.management.internal.HotSpotThreadImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.management.internal.OperatingSystemImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.unboundid.ldap.listener.InMemoryDirectoryServer" +}, +{ + "name":"com.uwetrottmann.tmdb2.Tmdb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "apiKey", + "parameterTypes": [] + }, + { + "name": "apiKey", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "collectionService", + "parameterTypes": [] + }, + { + "name": "configurationService", + "parameterTypes": [] + }, + { + "name": "discoverMovie", + "parameterTypes": [] + }, + { + "name": "discoverService", + "parameterTypes": [] + }, + { + "name": "discoverTv", + "parameterTypes": [] + }, + { + "name": "findService", + "parameterTypes": [] + }, + { + "name": "genreService", + "parameterTypes": [] + }, + { + "name": "moviesService", + "parameterTypes": [] + }, + { + "name": "personService", + "parameterTypes": [] + }, + { + "name": "reviewsService", + "parameterTypes": [] + }, + { + "name": "searchService", + "parameterTypes": [] + }, + { + "name": "tvEpisodesService", + "parameterTypes": [] + }, + { + "name": "tvSeasonsService", + "parameterTypes": [] + }, + { + "name": "tvService", + "parameterTypes": [] + } + ] +}, +{ + "name":"com.wavefront.sdk.common.WavefrontSender" +}, +{ + "name":"com.wavefront.sdk.common.application.ApplicationTags" +}, +{ + "name":"com.zaxxer.hikari.HikariConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addDataSourceProperty", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object" + ] + }, + { + "name": "addHealthCheckProperty", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "copyStateTo", + "parameterTypes": [ + "com.zaxxer.hikari.HikariConfig" + ] + }, + { + "name": "getCatalog", + "parameterTypes": [] + }, + { + "name": "getConnectionInitSql", + "parameterTypes": [] + }, + { + "name": "getConnectionTestQuery", + "parameterTypes": [] + }, + { + "name": "getConnectionTimeout", + "parameterTypes": [] + }, + { + "name": "getDataSource", + "parameterTypes": [] + }, + { + "name": "getDataSourceClassName", + "parameterTypes": [] + }, + { + "name": "getDataSourceJNDI", + "parameterTypes": [] + }, + { + "name": "getDataSourceProperties", + "parameterTypes": [] + }, + { + "name": "getDriverClassName", + "parameterTypes": [] + }, + { + "name": "getExceptionOverrideClassName", + "parameterTypes": [] + }, + { + "name": "getHealthCheckProperties", + "parameterTypes": [] + }, + { + "name": "getHealthCheckRegistry", + "parameterTypes": [] + }, + { + "name": "getIdleTimeout", + "parameterTypes": [] + }, + { + "name": "getInitializationFailTimeout", + "parameterTypes": [] + }, + { + "name": "getJdbcUrl", + "parameterTypes": [] + }, + { + "name": "getKeepaliveTime", + "parameterTypes": [] + }, + { + "name": "getLeakDetectionThreshold", + "parameterTypes": [] + }, + { + "name": "getMaxLifetime", + "parameterTypes": [] + }, + { + "name": "getMaximumPoolSize", + "parameterTypes": [] + }, + { + "name": "getMetricRegistry", + "parameterTypes": [] + }, + { + "name": "getMetricsTrackerFactory", + "parameterTypes": [] + }, + { + "name": "getMinimumIdle", + "parameterTypes": [] + }, + { + "name": "getPassword", + "parameterTypes": [] + }, + { + "name": "getPoolName", + "parameterTypes": [] + }, + { + "name": "getScheduledExecutor", + "parameterTypes": [] + }, + { + "name": "getSchema", + "parameterTypes": [] + }, + { + "name": "getThreadFactory", + "parameterTypes": [] + }, + { + "name": "getTransactionIsolation", + "parameterTypes": [] + }, + { + "name": "getUsername", + "parameterTypes": [] + }, + { + "name": "getValidationTimeout", + "parameterTypes": [] + }, + { + "name": "isAllowPoolSuspension", + "parameterTypes": [] + }, + { + "name": "isAutoCommit", + "parameterTypes": [] + }, + { + "name": "isIsolateInternalQueries", + "parameterTypes": [] + }, + { + "name": "isReadOnly", + "parameterTypes": [] + }, + { + "name": "isRegisterMbeans", + "parameterTypes": [] + }, + { + "name": "setAllowPoolSuspension", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setAutoCommit", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setCatalog", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConnectionInitSql", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConnectionTestQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConnectionTimeout", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setDataSource", + "parameterTypes": [ + "javax.sql.DataSource" + ] + }, + { + "name": "setDataSourceClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDataSourceJNDI", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDataSourceProperties", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setDriverClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setExceptionOverrideClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setHealthCheckProperties", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setIdleTimeout", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setInitializationFailTimeout", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setIsolateInternalQueries", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setJdbcUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setKeepaliveTime", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setLeakDetectionThreshold", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setMaxLifetime", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setMaximumPoolSize", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setMinimumIdle", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setPassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPoolName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setReadOnly", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRegisterMbeans", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setScheduledExecutor", + "parameterTypes": [ + "java.util.concurrent.ScheduledExecutorService" + ] + }, + { + "name": "setSchema", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setThreadFactory", + "parameterTypes": [ + "java.util.concurrent.ThreadFactory" + ] + }, + { + "name": "setTransactionIsolation", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUsername", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setValidationTimeout", + "parameterTypes": [ + "long" + ] + }, + { + "name": "validate", + "parameterTypes": [] + } + ] +}, +{ + "name":"com.zaxxer.hikari.HikariConfigMXBean", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.zaxxer.hikari.HikariDataSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "close", + "parameterTypes": [] + }, + { + "name": "getConnection", + "parameterTypes": [] + }, + { + "name": "getConnection", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "getLogWriter", + "parameterTypes": [] + }, + { + "name": "getLoginTimeout", + "parameterTypes": [] + }, + { + "name": "getParentLogger", + "parameterTypes": [] + }, + { + "name": "isWrapperFor", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setHealthCheckRegistry", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setLogWriter", + "parameterTypes": [ + "java.io.PrintWriter" + ] + }, + { + "name": "setLoginTimeout", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setMetricRegistry", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setMetricsTrackerFactory", + "parameterTypes": [ + "com.zaxxer.hikari.metrics.MetricsTrackerFactory" + ] + }, + { + "name": "toString", + "parameterTypes": [] + }, + { + "name": "unwrap", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"com.zaxxer.hikari.pool.HikariProxyConnection", + "methods":[{"name":"getSchema","parameterTypes":[] }] +}, +{ + "name":"float", + "queryAllDeclaredMethods":true +}, +{ + "name":"freemarker.template$Configuration" +}, +{ + "name":"freemarker.template.Configuration" +}, +{ + "name":"graphql.GraphQL" +}, +{ + "name":"groovy.lang$MetaClass" +}, +{ + "name":"groovy.lang.MetaClass" +}, +{ + "name":"groovy.text$TemplateEngine" +}, +{ + "name":"groovy.text.TemplateEngine" +}, +{ + "name":"groovy.text.markup.MarkupTemplateEngine" +}, +{ + "name":"io.lettuce.core.metrics.MicrometerCommandLatencyRecorder" +}, +{ + "name":"io.micrometer.appoptics.AppOpticsMeterRegistry" +}, +{ + "name":"io.micrometer.atlas.AtlasMeterRegistry" +}, +{ + "name":"io.micrometer.common.lang.NonNullApi", + "queryAllDeclaredMethods":true +}, +{ + "name":"io.micrometer.common.lang.NonNullFields", + "queryAllDeclaredMethods":true +}, +{ + "name":"io.micrometer.common.lang.Nullable", + "queryAllDeclaredMethods":true +}, +{ + "name":"io.micrometer.context$ContextSnapshot" +}, +{ + "name":"io.micrometer.context.ContextSnapshot" +}, +{ + "name":"io.micrometer.core.annotation.Timed" +}, +{ + "name":"io.micrometer.core.instrument.Clock", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"io.micrometer.core.instrument.Clock$1", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "monotonicTime", + "parameterTypes": [] + }, + { + "name": "wallTime", + "parameterTypes": [] + } + ] +}, +{ + "name":"io.micrometer.core.instrument.MeterRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "clear", + "parameterTypes": [] + }, + { + "name": "close", + "parameterTypes": [] + }, + { + "name": "config", + "parameterTypes": [] + }, + { + "name": "counter", + "parameterTypes": [ + "java.lang.String", + "java.lang.Iterable" + ] + }, + { + "name": "counter", + "parameterTypes": [ + "java.lang.String", + "java.lang.String[]" + ] + }, + { + "name": "find", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "forEachMeter", + "parameterTypes": [ + "java.util.function.Consumer" + ] + }, + { + "name": "gauge", + "parameterTypes": [ + "java.lang.String", + "java.lang.Iterable", + "java.lang.Number" + ] + }, + { + "name": "gauge", + "parameterTypes": [ + "java.lang.String", + "java.lang.Iterable", + "java.lang.Object", + "java.util.function.ToDoubleFunction" + ] + }, + { + "name": "gauge", + "parameterTypes": [ + "java.lang.String", + "java.lang.Number" + ] + }, + { + "name": "gauge", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.util.function.ToDoubleFunction" + ] + }, + { + "name": "gaugeCollectionSize", + "parameterTypes": [ + "java.lang.String", + "java.lang.Iterable", + "java.util.Collection" + ] + }, + { + "name": "gaugeMapSize", + "parameterTypes": [ + "java.lang.String", + "java.lang.Iterable", + "java.util.Map" + ] + }, + { + "name": "get", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getMeters", + "parameterTypes": [] + }, + { + "name": "isClosed", + "parameterTypes": [] + }, + { + "name": "more", + "parameterTypes": [] + }, + { + "name": "remove", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter$Id" + ] + }, + { + "name": "remove", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter" + ] + }, + { + "name": "removeByPreFilterId", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter$Id" + ] + }, + { + "name": "summary", + "parameterTypes": [ + "java.lang.String", + "java.lang.Iterable" + ] + }, + { + "name": "summary", + "parameterTypes": [ + "java.lang.String", + "java.lang.String[]" + ] + }, + { + "name": "timer", + "parameterTypes": [ + "java.lang.String", + "java.lang.Iterable" + ] + }, + { + "name": "timer", + "parameterTypes": [ + "java.lang.String", + "java.lang.String[]" + ] + } + ] +}, +{ + "name":"io.micrometer.core.instrument.binder.MeterBinder", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener" +}, +{ + "name":"io.micrometer.core.instrument.binder.jetty.JettyServerThreadPoolMetrics" +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics" +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.JvmGcMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "bindTo", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry" + ] + }, + { + "name": "close", + "parameterTypes": [] + } + ] +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "bindTo", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry" + ] + }, + { + "name": "close", + "parameterTypes": [] + } + ] +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.JvmInfoMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.kafka.KafkaClientMetrics" +}, +{ + "name":"io.micrometer.core.instrument.binder.logging.Log4j2Metrics" +}, +{ + "name":"io.micrometer.core.instrument.binder.logging.LogbackMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "bindTo", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry" + ] + }, + { + "name": "close", + "parameterTypes": [] + } + ] +}, +{ + "name":"io.micrometer.core.instrument.binder.system.FileDescriptorMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.system.ProcessorMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.system.UptimeMetrics", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"io.micrometer.core.instrument.binder.tomcat.TomcatMetrics" +}, +{ + "name":"io.micrometer.core.instrument.composite.CompositeMeterRegistry" +}, +{ + "name":"io.micrometer.core.instrument.config.MeterFilter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"map","parameterTypes":["io.micrometer.core.instrument.Meter$Id"] }] +}, +{ + "name":"io.micrometer.core.instrument.config.MeterFilter$9", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "accept", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter$Id" + ] + }, + { + "name": "configure", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter$Id", + "io.micrometer.core.instrument.distribution.DistributionStatisticConfig" + ] + } + ] +}, +{ + "name":"io.micrometer.core.instrument.config.MeterRegistryConfig", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"requireValid","parameterTypes":[] }] +}, +{ + "name":"io.micrometer.core.instrument.observation.DefaultMeterObservationHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "onEvent", + "parameterTypes": [ + "io.micrometer.observation.Observation$Event", + "io.micrometer.observation.Observation$Context" + ] + }, + { + "name": "onStart", + "parameterTypes": [ + "io.micrometer.observation.Observation$Context" + ] + }, + { + "name": "onStop", + "parameterTypes": [ + "io.micrometer.observation.Observation$Context" + ] + } + ] +}, +{ + "name":"io.micrometer.core.instrument.observation.MeterObservationHandler", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"supportsContext","parameterTypes":["io.micrometer.observation.Observation$Context"] }] +}, +{ + "name":"io.micrometer.core.instrument.simple.SimpleConfig", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"validate","parameterTypes":[] }] +}, +{ + "name":"io.micrometer.core.instrument.simple.SimpleMeterRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"io.micrometer.datadog.DatadogMeterRegistry" +}, +{ + "name":"io.micrometer.dynatrace.DynatraceMeterRegistry" +}, +{ + "name":"io.micrometer.elastic.ElasticMeterRegistry" +}, +{ + "name":"io.micrometer.ganglia.GangliaMeterRegistry" +}, +{ + "name":"io.micrometer.graphite.GraphiteMeterRegistry" +}, +{ + "name":"io.micrometer.humio.HumioMeterRegistry" +}, +{ + "name":"io.micrometer.influx.InfluxMeterRegistry" +}, +{ + "name":"io.micrometer.jmx.JmxMeterRegistry" +}, +{ + "name":"io.micrometer.kairos.KairosMeterRegistry" +}, +{ + "name":"io.micrometer.newrelic.NewRelicMeterRegistry" +}, +{ + "name":"io.micrometer.observation.Observation" +}, +{ + "name":"io.micrometer.observation.ObservationHandler", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "onError", + "parameterTypes": [ + "io.micrometer.observation.Observation$Context" + ] + }, + { + "name": "onScopeClosed", + "parameterTypes": [ + "io.micrometer.observation.Observation$Context" + ] + }, + { + "name": "onScopeOpened", + "parameterTypes": [ + "io.micrometer.observation.Observation$Context" + ] + } + ] +}, +{ + "name":"io.micrometer.observation.ObservationRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"io.micrometer.observation.SimpleObservationRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getCurrentObservation", + "parameterTypes": [] + }, + { + "name": "getCurrentObservationScope", + "parameterTypes": [] + }, + { + "name": "isNoop", + "parameterTypes": [] + }, + { + "name": "observationConfig", + "parameterTypes": [] + }, + { + "name": "setCurrentObservationScope", + "parameterTypes": [ + "io.micrometer.observation.Observation$Scope" + ] + } + ] +}, +{ + "name":"io.micrometer.prometheus.PrometheusMeterRegistry" +}, +{ + "name":"io.micrometer.registry.otlp.OtlpMeterRegistry" +}, +{ + "name":"io.micrometer.signalfx.SignalFxMeterRegistry" +}, +{ + "name":"io.micrometer.stackdriver.StackdriverMeterRegistry" +}, +{ + "name":"io.micrometer.statsd.StatsdMeterRegistry" +}, +{ + "name":"io.micrometer.tracing.Tracer" +}, +{ + "name":"io.micrometer.tracing.otel.bridge.OtelTracer" +}, +{ + "name":"io.netty.buffer.PooledByteBufAllocator" +}, +{ + "name":"io.netty.util.NettyRuntime" +}, +{ + "name":"io.r2dbc.pool.ConnectionPool" +}, +{ + "name":"io.r2dbc.spi.ConnectionFactory" +}, +{ + "name":"io.reactivex.rxjava3.core$Flowable" +}, +{ + "name":"io.reactivex.rxjava3.core.Flowable" +}, +{ + "name":"io.rsocket.RSocket" +}, +{ + "name":"io.rsocket.core.RSocketServer" +}, +{ + "name":"io.smallrye.mutiny$Multi" +}, +{ + "name":"io.smallrye.mutiny.Multi" +}, +{ + "name":"io.undertow.Undertow" +}, +{ + "name":"io.undertow.websockets.jsr$ServerWebSocketContainer" +}, +{ + "name":"io.undertow.websockets.jsr.Bootstrap" +}, +{ + "name":"io.undertow.websockets.jsr.ServerWebSocketContainer" +}, +{ + "name":"io.vavr.control$Option" +}, +{ + "name":"io.vavr.control$Try" +}, +{ + "name":"io.vavr.control.Option" +}, +{ + "name":"io.vavr.control.Try" +}, +{ + "name":"jakarta.activation.MimeType" +}, +{ + "name":"jakarta.annotation.ManagedBean" +}, +{ + "name":"jakarta.annotation.PostConstruct", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.annotation.PreDestroy", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.ejb$Asynchronous" +}, +{ + "name":"jakarta.ejb$EJB" +}, +{ + "name":"jakarta.ejb$TransactionAttribute" +}, +{ + "name":"jakarta.ejb.Asynchronous" +}, +{ + "name":"jakarta.ejb.EJB" +}, +{ + "name":"jakarta.ejb.TransactionAttribute" +}, +{ + "name":"jakarta.enterprise.concurrent$Asynchronous" +}, +{ + "name":"jakarta.enterprise.concurrent$ManagedExecutorService" +}, +{ + "name":"jakarta.enterprise.concurrent$ManagedScheduledExecutorService" +}, +{ + "name":"jakarta.enterprise.concurrent.Asynchronous" +}, +{ + "name":"jakarta.enterprise.concurrent.ManagedExecutorService" +}, +{ + "name":"jakarta.enterprise.concurrent.ManagedScheduledExecutorService" +}, +{ + "name":"jakarta.faces.context$FacesContext" +}, +{ + "name":"jakarta.faces.context.FacesContext" +}, +{ + "name":"jakarta.inject.Inject" +}, +{ + "name":"jakarta.inject.Named" +}, +{ + "name":"jakarta.inject.Provider" +}, +{ + "name":"jakarta.inject.Qualifier" +}, +{ + "name":"jakarta.jms.ConnectionFactory" +}, +{ + "name":"jakarta.jms.Message" +}, +{ + "name":"jakarta.json.bind$Jsonb" +}, +{ + "name":"jakarta.json.bind.Jsonb" +}, +{ + "name":"jakarta.mail.internet.MimeMessage" +}, +{ + "name":"jakarta.persistence.AttributeConverter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.persistence.Column", + "queryAllDeclaredMethods":true, + "methods":[{"name":"updatable","parameterTypes":[] }] +}, +{ + "name":"jakarta.persistence.Convert", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.Converter", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.Entity", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.EntityManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "clear", + "parameterTypes": [] + }, + { + "name": "close", + "parameterTypes": [] + }, + { + "name": "contains", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "createEntityGraph", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "createEntityGraph", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNamedQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNamedQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "createNamedStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaDelete" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaQuery" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaUpdate" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "createStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class[]" + ] + }, + { + "name": "createStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.String[]" + ] + }, + { + "name": "detach", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "find", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object" + ] + }, + { + "name": "find", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object", + "jakarta.persistence.LockModeType" + ] + }, + { + "name": "find", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object", + "jakarta.persistence.LockModeType", + "java.util.Map" + ] + }, + { + "name": "find", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object", + "java.util.Map" + ] + }, + { + "name": "flush", + "parameterTypes": [] + }, + { + "name": "getCriteriaBuilder", + "parameterTypes": [] + }, + { + "name": "getDelegate", + "parameterTypes": [] + }, + { + "name": "getEntityGraph", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getEntityGraphs", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getEntityManagerFactory", + "parameterTypes": [] + }, + { + "name": "getFlushMode", + "parameterTypes": [] + }, + { + "name": "getLockMode", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getMetamodel", + "parameterTypes": [] + }, + { + "name": "getProperties", + "parameterTypes": [] + }, + { + "name": "getReference", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object" + ] + }, + { + "name": "getTransaction", + "parameterTypes": [] + }, + { + "name": "isJoinedToTransaction", + "parameterTypes": [] + }, + { + "name": "isOpen", + "parameterTypes": [] + }, + { + "name": "joinTransaction", + "parameterTypes": [] + }, + { + "name": "lock", + "parameterTypes": [ + "java.lang.Object", + "jakarta.persistence.LockModeType" + ] + }, + { + "name": "lock", + "parameterTypes": [ + "java.lang.Object", + "jakarta.persistence.LockModeType", + "java.util.Map" + ] + }, + { + "name": "merge", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "persist", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "refresh", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "refresh", + "parameterTypes": [ + "java.lang.Object", + "jakarta.persistence.LockModeType" + ] + }, + { + "name": "refresh", + "parameterTypes": [ + "java.lang.Object", + "jakarta.persistence.LockModeType", + "java.util.Map" + ] + }, + { + "name": "refresh", + "parameterTypes": [ + "java.lang.Object", + "java.util.Map" + ] + }, + { + "name": "remove", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setFlushMode", + "parameterTypes": [ + "jakarta.persistence.FlushModeType" + ] + }, + { + "name": "setProperty", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object" + ] + }, + { + "name": "unwrap", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"jakarta.persistence.EntityManagerFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addNamedEntityGraph", + "parameterTypes": [ + "java.lang.String", + "jakarta.persistence.EntityGraph" + ] + }, + { + "name": "addNamedQuery", + "parameterTypes": [ + "java.lang.String", + "jakarta.persistence.Query" + ] + }, + { + "name": "createEntityManager", + "parameterTypes": [] + }, + { + "name": "createEntityManager", + "parameterTypes": [ + "jakarta.persistence.SynchronizationType" + ] + }, + { + "name": "createEntityManager", + "parameterTypes": [ + "jakarta.persistence.SynchronizationType", + "java.util.Map" + ] + }, + { + "name": "createEntityManager", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "getCriteriaBuilder", + "parameterTypes": [] + }, + { + "name": "getMetamodel", + "parameterTypes": [] + }, + { + "name": "getPersistenceUnitUtil", + "parameterTypes": [] + }, + { + "name": "getProperties", + "parameterTypes": [] + }, + { + "name": "isOpen", + "parameterTypes": [] + }, + { + "name": "unwrap", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"jakarta.persistence.Enumerated", + "queryAllDeclaredMethods":true, + "methods":[{"name":"value","parameterTypes":[] }] +}, +{ + "name":"jakarta.persistence.ForeignKey", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.GeneratedValue", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.Id", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.Index", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.JoinColumn", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.ManyToOne", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "optional", + "parameterTypes": [] + }, + { + "name": "targetEntity", + "parameterTypes": [] + } + ] +}, +{ + "name":"jakarta.persistence.OneToMany", + "queryAllDeclaredMethods":true, + "methods":[{"name":"targetEntity","parameterTypes":[] }] +}, +{ + "name":"jakarta.persistence.OneToOne", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "mappedBy", + "parameterTypes": [] + }, + { + "name": "targetEntity", + "parameterTypes": [] + } + ] +}, +{ + "name":"jakarta.persistence.Persistence", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getPersistenceUtil", + "parameterTypes": [] + } + ] +}, +{ + "name":"jakarta.persistence.PersistenceContext", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.PersistenceProperty", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.Query", + "methods":[{"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"jakarta.persistence.SequenceGenerator", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.Table", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.persistence.UniqueConstraint", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.servlet.Filter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.servlet.GenericFilter", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.servlet.GenericServlet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getInitParameter", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getInitParameterNames", + "parameterTypes": [] + }, + { + "name": "getServletConfig", + "parameterTypes": [] + }, + { + "name": "getServletContext", + "parameterTypes": [] + }, + { + "name": "getServletInfo", + "parameterTypes": [] + }, + { + "name": "log", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "log", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] + } + ] +}, +{ + "name":"jakarta.servlet.MultipartConfigElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.servlet.Servlet", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.servlet.ServletConfig", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.servlet.ServletContext", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.servlet.ServletRegistration" +}, +{ + "name":"jakarta.servlet.ServletRequest" +}, +{ + "name":"jakarta.servlet.http.HttpServlet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "init", + "parameterTypes": [ + "jakarta.servlet.ServletConfig" + ] + }, + { + "name": "service", + "parameterTypes": [ + "jakarta.servlet.ServletRequest", + "jakarta.servlet.ServletResponse" + ] + } + ] +}, +{ + "name":"jakarta.servlet.http.PushBuilder" +}, +{ + "name":"jakarta.transaction.Transaction" +}, +{ + "name":"jakarta.transaction.TransactionManager" +}, +{ + "name":"jakarta.transaction.Transactional" +}, +{ + "name":"jakarta.validation.Constraint", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.validation.ConstraintViolation" +}, +{ + "name":"jakarta.validation.Validator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.validation.ValidatorFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"jakarta.validation.bootstrap.GenericBootstrap" +}, +{ + "name":"jakarta.validation.constraints.NotEmpty", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "groups", + "parameterTypes": [] + }, + { + "name": "message", + "parameterTypes": [] + }, + { + "name": "payload", + "parameterTypes": [] + } + ] +}, +{ + "name":"jakarta.validation.constraints.NotNull", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "groups", + "parameterTypes": [] + }, + { + "name": "message", + "parameterTypes": [] + }, + { + "name": "payload", + "parameterTypes": [] + } + ] +}, +{ + "name":"jakarta.validation.executable.ExecutableValidator" +}, +{ + "name":"jakarta.websocket.server.ServerContainer" +}, +{ + "name":"jakarta.xml.bind.Binder" +}, +{ + "name":"jakarta.xml.bind.annotation.XmlAccessType" +}, +{ + "name":"jakarta.xml.bind.annotation.XmlAccessorType", + "queryAllDeclaredMethods":true, + "methods":[{"name":"value","parameterTypes":[] }] +}, +{ + "name":"jakarta.xml.bind.annotation.XmlElement", + "methods":[{"name":"type","parameterTypes":[] }] +}, +{ + "name":"jakarta.xml.bind.annotation.XmlNs", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.xml.bind.annotation.XmlNsForm" +}, +{ + "name":"jakarta.xml.bind.annotation.XmlRootElement", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.xml.bind.annotation.XmlSchema", + "queryAllDeclaredMethods":true +}, +{ + "name":"jakarta.xml.bind.annotation.XmlSeeAlso", + "queryAllDeclaredMethods":true, + "methods":[{"name":"value","parameterTypes":[] }] +}, +{ + "name":"jakarta.xml.bind.annotation.XmlType", + "queryAllDeclaredMethods":true, + "methods":[{"name":"factoryClass","parameterTypes":[] }] +}, +{ + "name":"jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter", + "methods": [ + { + "name": "type", + "parameterTypes": [] + }, + { + "name": "value", + "parameterTypes": [] + } + ] +}, +{ + "name":"jakarta.xml.ws.WebServiceRef" +}, +{ + "name":"java.io.Closeable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.io.FilePermission" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.Boolean", + "queryAllDeclaredMethods":true, + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Byte", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Character", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Class", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getAnnotatedInterfaces", + "parameterTypes": [] + }, + { + "name": "getAnnotatedSuperclass", + "parameterTypes": [] + }, + { + "name": "getDeclaredMethod", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class[]" + ] + }, + { + "name": "getMethod", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class[]" + ] + }, + { + "name": "getModule", + "parameterTypes": [] + }, + { + "name": "getNestHost", + "parameterTypes": [] + }, + { + "name": "getNestMembers", + "parameterTypes": [] + }, + { + "name": "getPermittedSubclasses", + "parameterTypes": [] + }, + { + "name": "getRecordComponents", + "parameterTypes": [] + }, + { + "name": "isNestmateOf", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "isRecord", + "parameterTypes": [] + }, + { + "name": "isSealed", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.ClassLoader", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "defineClass", + "parameterTypes": [ + "java.lang.String", + "byte[]", + "int", + "int", + "java.security.ProtectionDomain" + ] + }, + { + "name": "getUnnamedModule", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.Comparable", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Deprecated", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.Double", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Float", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Integer", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Iterable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "forEach", + "parameterTypes": [ + "java.util.function.Consumer" + ] + }, + { + "name": "spliterator", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.Long", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.Module", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addExports", + "parameterTypes": [ + "java.lang.String", + "java.lang.Module" + ] + }, + { + "name": "isExported", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "isNamed", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.Object", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "clone", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getClass", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "notify", + "parameterTypes": [] + }, + { + "name": "notifyAll", + "parameterTypes": [] + }, + { + "name": "toString", + "parameterTypes": [] + }, + { + "name": "wait", + "parameterTypes": [] + }, + { + "name": "wait", + "parameterTypes": [ + "long" + ] + }, + { + "name": "wait", + "parameterTypes": [ + "long", + "int" + ] + } + ] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"major","parameterTypes":[] }] +}, +{ + "name":"java.lang.RuntimePermission" +}, +{ + "name":"java.lang.Short", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.StackTraceElement", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.String", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.Throwable", + "allDeclaredFields":true +}, +{ + "name":"java.lang.Void", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.WrongThreadException" +}, +{ + "name":"java.lang.annotation.Annotation", + "methods":[{"name":"annotationType","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Documented", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.annotation.Inherited", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.annotation.Repeatable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.constant.Constable", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.invoke.MethodHandles", + "queryAllDeclaredMethods":true, + "methods":[{"name":"privateLookupIn","parameterTypes":["java.lang.Class","java.lang.invoke.MethodHandles$Lookup"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods": [ + { + "name": "defineClass", + "parameterTypes": [ + "byte[]" + ] + }, + { + "name": "lookupClass", + "parameterTypes": [] + }, + { + "name": "lookupModes", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.invoke.TypeDescriptor$OfField", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.BufferPoolMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.ClassLoadingMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.CompilationMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.LockInfo", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.ManagementFactory", + "methods":[{"name":"getRuntimeMXBean","parameterTypes":[] }] +}, +{ + "name":"java.lang.management.ManagementPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.management.MemoryMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.MemoryManagerMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.MemoryPoolMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.MemoryUsage", + "queryAllPublicMethods":true, + "methods":[{"name":"from","parameterTypes":["javax.management.openmbean.CompositeData"] }] +}, +{ + "name":"java.lang.management.MonitorInfo", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.management.PlatformLoggingMXBean", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getLoggerLevel", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getLoggerNames", + "parameterTypes": [] + }, + { + "name": "getParentLoggerName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setLoggerLevel", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + } + ] +}, +{ + "name":"java.lang.management.RuntimeMXBean", + "queryAllPublicMethods":true, + "methods":[{"name":"getInputArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.management.ThreadInfo", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedElement", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.reflect.AnnotatedParameterizedType", + "methods":[{"name":"getAnnotatedActualTypeArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods": [ + { + "name": "getAnnotatedExceptionTypes", + "parameterTypes": [] + }, + { + "name": "getAnnotatedParameterTypes", + "parameterTypes": [] + }, + { + "name": "getAnnotatedReceiverType", + "parameterTypes": [] + }, + { + "name": "getParameterCount", + "parameterTypes": [] + }, + { + "name": "getParameters", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.reflect.GenericDeclaration", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods": [ + { + "name": "getModifiers", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "isNamePresent", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.reflect.ParameterizedType", + "methods": [ + { + "name": "getActualTypeArguments", + "parameterTypes": [] + }, + { + "name": "getOwnerType", + "parameterTypes": [] + }, + { + "name": "getRawType", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.reflect.Proxy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.reflect.Type", + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.reflect.TypeVariable", + "methods": [ + { + "name": "getBounds", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.lang.reflect.WildcardType", + "methods": [ + { + "name": "getLowerBounds", + "parameterTypes": [] + }, + { + "name": "getUpperBounds", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.math.BigDecimal" +}, +{ + "name":"java.math.BigInteger" +}, +{ + "name":"java.net.NetPermission" +}, +{ + "name":"java.net.SocketPermission" +}, +{ + "name":"java.net.URLClassLoader", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.net.URLPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.net.UnixDomainSocketAddress", + "methods":[{"name":"of","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.nio.channels.ServerSocketChannel", + "methods":[{"name":"open","parameterTypes":["java.net.ProtocolFamily"] }] +}, +{ + "name":"java.nio.channels.SocketChannel", + "methods":[{"name":"open","parameterTypes":["java.net.ProtocolFamily"] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.AllPermission" +}, +{ + "name":"java.security.KeyStoreSpi" +}, +{ + "name":"java.security.SecureClassLoader", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.security.SecurityPermission" +}, +{ + "name":"java.security.interfaces.ECPrivateKey" +}, +{ + "name":"java.security.interfaces.ECPublicKey" +}, +{ + "name":"java.security.interfaces.RSAPrivateKey" +}, +{ + "name":"java.security.interfaces.RSAPublicKey" +}, +{ + "name":"java.sql.Connection", + "methods": [ + { + "name": "createClob", + "parameterTypes": [] + }, + { + "name": "getMetaData", + "parameterTypes": [] + } + ] +}, +{ + "name":"java.sql.Date" +}, +{ + "name":"java.sql.Driver" +}, +{ + "name":"java.sql.DriverManager" +}, +{ + "name":"java.sql.Timestamp" +}, +{ + "name":"java.sql.Types", + "allPublicFields":true +}, +{ + "name":"java.sql.Wrapper", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.time.Instant" +}, +{ + "name":"java.util.ArrayList", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"java.util.Date" +}, +{ + "name":"java.util.Enumeration" +}, +{ + "name":"java.util.EventListener", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.util.HashMap", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name": "java.util.HashSet" +}, +{ + "name":"java.util.PropertyPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.util.concurrent.Callable", + "methods":[{"name":"call","parameterTypes":[] }] +}, +{ + "name":"java.util.concurrent.Executor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.util.concurrent.ScheduledExecutorService" +}, +{ + "name":"java.util.concurrent.ThreadFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.util.function.Supplier", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.util.logging.LogManager", + "methods":[{"name":"getLoggingMXBean","parameterTypes":[] }] +}, +{ + "name":"java.util.logging.LoggingMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"java.util.logging.SimpleFormatter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"javafx.beans.value.ObservableValue" +}, +{ + "name":"javax.annotation$PostConstruct" +}, +{ + "name":"javax.annotation.Nonnull", + "queryAllDeclaredMethods":true +}, +{ + "name":"javax.annotation.Nullable", + "queryAllDeclaredMethods":true +}, +{ + "name":"javax.annotation.PostConstruct" +}, +{ + "name":"javax.annotation.meta.TypeQualifier", + "queryAllDeclaredMethods":true +}, +{ + "name":"javax.annotation.meta.TypeQualifierDefault", + "queryAllDeclaredMethods":true +}, +{ + "name":"javax.cache$Cache" +}, +{ + "name":"javax.cache.Cache" +}, +{ + "name":"javax.cache.CacheManager" +}, +{ + "name":"javax.inject$Inject" +}, +{ + "name":"javax.inject.Inject" +}, +{ + "name":"javax.management.MBeanOperationInfo", + "queryAllPublicMethods":true, + "methods":[{"name":"getSignature","parameterTypes":[] }] +}, +{ + "name":"javax.management.MBeanServerBuilder", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"javax.management.ObjectName" +}, +{ + "name":"javax.management.openmbean.CompositeData" +}, +{ + "name":"javax.management.openmbean.OpenMBeanOperationInfoSupport" +}, +{ + "name":"javax.management.openmbean.TabularData" +}, +{ + "name":"javax.money$MonetaryAmount" +}, +{ + "name":"javax.money.MonetaryAmount" +}, +{ + "name":"javax.naming.InitialContext" +}, +{ + "name":"javax.naming.Referenceable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getReference","parameterTypes":[] }] +}, +{ + "name":"javax.naming.ldap.LdapContext" +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"javax.smartcardio.CardPermission" +}, +{ + "name":"javax.sql.CommonDataSource", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"createShardingKeyBuilder","parameterTypes":[] }] +}, +{ + "name":"javax.sql.DataSource", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"createConnectionBuilder","parameterTypes":[] }] +}, +{ + "name":"javax.sql.XADataSource" +}, +{ + "name":"jdk.management.jfr.ConfigurationInfo", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.EventTypeInfo", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.FlightRecorderMXBean", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.FlightRecorderMXBeanImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"jdk.management.jfr.RecordingInfo", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.management.jfr.SettingDescriptorInfo", + "queryAllPublicMethods":true +}, +{ + "name":"kotlin.Metadata" +}, +{ + "name":"kotlin.coroutines.Continuation" +}, +{ + "name":"kotlin.reflect.full$KClasses" +}, +{ + "name":"kotlin.reflect.full.KClasses" +}, +{ + "name":"kotlinx.coroutines.reactor$MonoKt" +}, +{ + "name":"kotlinx.coroutines.reactor.MonoKt" +}, +{ + "name":"kotlinx.serialization.cbor$Cbor" +}, +{ + "name":"kotlinx.serialization.cbor.Cbor" +}, +{ + "name":"kotlinx.serialization.json$Json" +}, +{ + "name":"kotlinx.serialization.json.Json" +}, +{ + "name":"kotlinx.serialization.protobuf$ProtoBuf" +}, +{ + "name":"kotlinx.serialization.protobuf.ProtoBuf" +}, +{ + "name":"liquibase.change.DatabaseChange" +}, +{ + "name":"liquibase.integration.spring.SpringLiquibase" +}, +{ + "name":"net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$Executable", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.method.ParameterDescription$ForLoadedParameter$Parameter", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.method.ParameterList$ForLoadedExecutable$Executable", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.type.TypeDefinition$Sort$AnnotatedType", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.type.TypeDescription$ForLoadedType$Dispatcher", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$Delegator$ForLoadedExecutableExceptionType$Dispatcher", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$Delegator$ForLoadedExecutableParameterType$Dispatcher", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$Delegator$ForLoadedMethodReturnType$Dispatcher", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$ForComponentType$AnnotatedParameterizedType", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.description.type.TypeDescription$Generic$AnnotationReader$ForTypeArgument$AnnotatedParameterizedType", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles$Lookup", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.AllArguments", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "includeSelf", + "parameterTypes": [] + }, + { + "name": "nullIfEmpty", + "parameterTypes": [] + }, + { + "name": "value", + "parameterTypes": [] + } + ] +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.Argument", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.BindingPriority", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.Default", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.DefaultCall", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.DefaultMethod", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.FieldValue", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "declaringType", + "parameterTypes": [] + }, + { + "name": "value", + "parameterTypes": [] + } + ] +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.Origin", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "cache", + "parameterTypes": [] + }, + { + "name": "privileged", + "parameterTypes": [] + } + ] +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.StubValue", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.Super", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.SuperCall", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.SuperMethod", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"net.bytebuddy.implementation.bind.annotation.This", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"optional","parameterTypes":[] }] +}, +{ + "name":"net.bytebuddy.utility.Invoker", + "queryAllPublicMethods":true +}, +{ + "name":"net.bytebuddy.utility.Invoker$Dispatcher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"net.logstash.logback.encoder.LogstashEncoder", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setIncludeContext", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"net.sf.jsqlparser.parser.JSqlParser" +}, +{ + "name":"net.sourceforge.htmlunit.corejs.javascript.ScriptableObject", + "methods":[{"name":"getExternalArrayLength","parameterTypes":[] }] +}, +{ + "name":"nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect" +}, +{ + "name":"oracle.jdbc.OracleConnection" +}, +{ + "name":"oracle.ucp.jdbc.PoolDataSource" +}, +{ + "name":"oracle.ucp.jdbc.PoolDataSourceImpl" +}, +{ + "name":"org.aopalliance.aop.Advice", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.aopalliance.intercept.Interceptor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.aopalliance.intercept.MethodInterceptor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.apache.catalina.Manager" +}, +{ + "name":"org.apache.catalina.core.ApplicationContextFacade", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addFilter", + "parameterTypes": [ + "java.lang.String", + "jakarta.servlet.Filter" + ] + }, + { + "name": "addFilter", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "addFilter", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "addJspFile", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "addListener", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "addListener", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "addListener", + "parameterTypes": [ + "java.util.EventListener" + ] + }, + { + "name": "addServlet", + "parameterTypes": [ + "java.lang.String", + "jakarta.servlet.Servlet" + ] + }, + { + "name": "addServlet", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "addServlet", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "createFilter", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "createListener", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "createServlet", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "declareRoles", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "getAttribute", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getAttributeNames", + "parameterTypes": [] + }, + { + "name": "getClassLoader", + "parameterTypes": [] + }, + { + "name": "getContext", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getContextPath", + "parameterTypes": [] + }, + { + "name": "getDefaultSessionTrackingModes", + "parameterTypes": [] + }, + { + "name": "getEffectiveMajorVersion", + "parameterTypes": [] + }, + { + "name": "getEffectiveMinorVersion", + "parameterTypes": [] + }, + { + "name": "getEffectiveSessionTrackingModes", + "parameterTypes": [] + }, + { + "name": "getFilterRegistration", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getFilterRegistrations", + "parameterTypes": [] + }, + { + "name": "getInitParameter", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getInitParameterNames", + "parameterTypes": [] + }, + { + "name": "getJspConfigDescriptor", + "parameterTypes": [] + }, + { + "name": "getMajorVersion", + "parameterTypes": [] + }, + { + "name": "getMimeType", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getMinorVersion", + "parameterTypes": [] + }, + { + "name": "getNamedDispatcher", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getRealPath", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getRequestCharacterEncoding", + "parameterTypes": [] + }, + { + "name": "getRequestDispatcher", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getResource", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getResourceAsStream", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getResourcePaths", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getResponseCharacterEncoding", + "parameterTypes": [] + }, + { + "name": "getServerInfo", + "parameterTypes": [] + }, + { + "name": "getServletContextName", + "parameterTypes": [] + }, + { + "name": "getServletRegistration", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getServletRegistrations", + "parameterTypes": [] + }, + { + "name": "getSessionCookieConfig", + "parameterTypes": [] + }, + { + "name": "getSessionTimeout", + "parameterTypes": [] + }, + { + "name": "getVirtualServerName", + "parameterTypes": [] + }, + { + "name": "log", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "log", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] + }, + { + "name": "removeAttribute", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setAttribute", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object" + ] + }, + { + "name": "setInitParameter", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "setRequestCharacterEncoding", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setResponseCharacterEncoding", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSessionTimeout", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setSessionTrackingModes", + "parameterTypes": [ + "java.util.Set" + ] + } + ] +}, +{ + "name":"org.apache.catalina.loader.JdbcLeakPrevention", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "clearJdbcDriverRegistrations", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.apache.catalina.startup.Tomcat" +}, +{ + "name":"org.apache.catalina.util.CharsetMapper", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.commons.dbcp2.BasicDataSource" +}, +{ + "name":"org.apache.coyote.AbstractProtocol", + "methods": [ + { + "name": "getAddress", + "parameterTypes": [] + }, + { + "name": "getLocalPort", + "parameterTypes": [] + }, + { + "name": "setPort", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setProperty", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.apache.coyote.UpgradeProtocol" +}, +{ + "name":"org.apache.coyote.http11.AbstractHttp11Protocol", + "methods":[{"name":"isSSLEnabled","parameterTypes":[] }] +}, +{ + "name":"org.apache.coyote.http11.Http11NioProtocol", + "queryAllPublicMethods":true +}, +{ + "name":"org.apache.el.ExpressionFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.http.impl.client.HttpClientBuilder", + "fields": [ + { + "name": "connTimeToLive" + }, + { + "name": "connTimeToLiveTimeUnit" + }, + { + "name": "defaultConnectionConfig" + }, + { + "name": "defaultSocketConfig" + }, + { + "name": "dnsResolver" + }, + { + "name": "hostnameVerifier" + }, + { + "name": "maxConnPerRoute" + }, + { + "name": "maxConnTotal" + }, + { + "name": "publicSuffixMatcher" + }, + { + "name": "sslContext" + }, + { + "name": "sslSocketFactory" + }, + { + "name": "systemProperties" + } + ] +}, +{ + "name":"org.apache.logging.log4j.core.LoggerContext" +}, +{ + "name":"org.apache.logging.log4j.core.impl$Log4jContextFactory" +}, +{ + "name":"org.apache.logging.log4j.core.impl.Log4jContextFactory" +}, +{ + "name":"org.apache.logging.log4j.spi.ExtendedLogger" +}, +{ + "name":"org.apache.logging.slf4j.SLF4JProvider" +}, +{ + "name":"org.apache.tomcat.jdbc.pool.DataSource" +}, +{ + "name":"org.apache.tomcat.util.net.AbstractEndpoint", + "methods":[{"name":"setBindOnInit","parameterTypes":["boolean"] }] +}, +{ + "name":"org.apache.tomcat.util.net.NioEndpoint", + "queryAllPublicMethods":true +}, +{ + "name":"org.apache.tomcat.websocket.server.WsFilter", + "queryAllDeclaredMethods":true +}, +{ + "name": "org.apache.tomcat.websocket.server.WsHttpUpgradeHandler" +}, +{ + "name":"org.apache.tomcat.websocket.server.WsSci" +}, +{ + "name":"org.apache.xerces.impl.dv.dtd.DTDDVFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.xerces.parsers.XIncludeAwareParserConfiguration", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.cache2k.Cache2kBuilder" +}, +{ + "name":"org.cache2k.extra.micrometer.Cache2kCacheMetrics" +}, +{ + "name":"org.cache2k.extra.spring.SpringCache2kCache" +}, +{ + "name":"org.eclipse.collections.api.list$ImmutableList" +}, +{ + "name":"org.eclipse.collections.api.list.ImmutableList" +}, +{ + "name":"org.eclipse.core.runtime$FileLocator" +}, +{ + "name":"org.eclipse.core.runtime.FileLocator" +}, +{ + "name":"org.eclipse.jetty.server.Server" +}, +{ + "name":"org.eclipse.jetty.util.Loader" +}, +{ + "name":"org.eclipse.jetty.webapp.WebAppContext" +}, +{ + "name":"org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer" +}, +{ + "name":"org.eclipse.jetty.websocket.server$JettyWebSocketServerContainer" +}, +{ + "name":"org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer" +}, +{ + "name":"org.eclipse.persistence.internal.jpa.deployment$JavaSECMPInitializerAgent" +}, +{ + "name":"org.eclipse.persistence.internal.jpa.deployment.JavaSECMPInitializerAgent" +}, +{ + "name":"org.elasticsearch.client.RestClient" +}, +{ + "name":"org.elasticsearch.client.RestClientBuilder" +}, +{ + "name":"org.flywaydb.core.Flyway", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.flywaydb.core.internal.logging.slf4j.Slf4jLogCreator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.glassfish.jaxb.core.v2.model.nav.ReflectionNavigator", + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.glassfish.jaxb.runtime.v2.runtime.property.ArrayElementLeafProperty", + "queryAllPublicConstructors":true +}, +{ + "name":"org.glassfish.jaxb.runtime.v2.runtime.property.ArrayElementNodeProperty", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.glassfish.jaxb.runtime.v2.runtime.JAXBContextImpl","org.glassfish.jaxb.runtime.v2.model.runtime.RuntimeElementPropertyInfo"] }] +}, +{ + "name":"org.glassfish.jaxb.runtime.v2.runtime.property.ArrayReferenceNodeProperty", + "queryAllPublicConstructors":true +}, +{ + "name":"org.glassfish.jaxb.runtime.v2.runtime.property.SingleElementLeafProperty", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.glassfish.jaxb.runtime.v2.runtime.JAXBContextImpl","org.glassfish.jaxb.runtime.v2.model.runtime.RuntimeElementPropertyInfo"] }] +}, +{ + "name":"org.glassfish.jaxb.runtime.v2.runtime.property.SingleElementNodeProperty", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.glassfish.jaxb.runtime.v2.runtime.JAXBContextImpl","org.glassfish.jaxb.runtime.v2.model.runtime.RuntimeElementPropertyInfo"] }] +}, +{ + "name":"org.glassfish.jaxb.runtime.v2.runtime.property.SingleMapNodeProperty", + "queryAllPublicConstructors":true +}, +{ + "name":"org.glassfish.jaxb.runtime.v2.runtime.property.SingleReferenceNodeProperty", + "queryAllPublicConstructors":true +}, +{ + "name":"org.glassfish.jersey.server.ResourceConfig" +}, +{ + "name":"org.glassfish.jersey.server.spring.SpringComponentProvider" +}, +{ + "name":"org.glassfish.tyrus.servlet$TyrusHttpUpgradeHandler" +}, +{ + "name":"org.glassfish.tyrus.servlet.TyrusHttpUpgradeHandler" +}, +{ + "name":"org.graalvm.nativeimage.ImageInfo", + "methods":[{"name":"inImageCode","parameterTypes":[] }] +}, +{ + "name":"org.h2.Driver", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.h2.jdbc.JdbcClob" +}, +{ + "name":"org.h2.jdbc.JdbcLob", + "methods":[{"name":"free","parameterTypes":[] }] +}, +{ + "name":"org.h2.server.web.JakartaWebServlet" +}, +{ + "name":"org.hibernate.Session", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "clear", + "parameterTypes": [] + }, + { + "name": "createEntityGraph", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "createEntityGraph", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNamedQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNamedQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaDelete" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaQuery" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaUpdate" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "detach", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "flush", + "parameterTypes": [] + }, + { + "name": "getEntityGraph", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getEntityGraphs", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getFlushMode", + "parameterTypes": [] + }, + { + "name": "getReference", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object" + ] + }, + { + "name": "load", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object" + ] + }, + { + "name": "merge", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "persist", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "refresh", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "remove", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setFlushMode", + "parameterTypes": [ + "jakarta.persistence.FlushModeType" + ] + } + ] +}, +{ + "name":"org.hibernate.SessionFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "close", + "parameterTypes": [] + }, + { + "name": "getCache", + "parameterTypes": [] + }, + { + "name": "openSession", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.hibernate.SharedSessionContract", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "beginTransaction", + "parameterTypes": [] + }, + { + "name": "close", + "parameterTypes": [] + }, + { + "name": "createNamedStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createStoredProcedureCall", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createStoredProcedureCall", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class[]" + ] + }, + { + "name": "createStoredProcedureCall", + "parameterTypes": [ + "java.lang.String", + "java.lang.String[]" + ] + }, + { + "name": "createStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class[]" + ] + }, + { + "name": "createStoredProcedureQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.String[]" + ] + }, + { + "name": "doReturningWork", + "parameterTypes": [ + "org.hibernate.jdbc.ReturningWork" + ] + }, + { + "name": "doWork", + "parameterTypes": [ + "org.hibernate.jdbc.Work" + ] + }, + { + "name": "getCriteriaBuilder", + "parameterTypes": [] + }, + { + "name": "getJdbcBatchSize", + "parameterTypes": [] + }, + { + "name": "getNamedProcedureCall", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getTenantIdentifier", + "parameterTypes": [] + }, + { + "name": "getTransaction", + "parameterTypes": [] + }, + { + "name": "isConnected", + "parameterTypes": [] + }, + { + "name": "isOpen", + "parameterTypes": [] + }, + { + "name": "setJdbcBatchSize", + "parameterTypes": [ + "java.lang.Integer" + ] + } + ] +}, +{ + "name":"org.hibernate.annotations.GenericGenerator", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.hibernate.annotations.LazyCollection", + "queryAllDeclaredMethods":true, + "methods":[{"name":"value","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.annotations.OnDelete", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.hibernate.annotations.Parameter", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.hibernate.boot.archive.scan.internal.DisabledScanner", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.boot.cfgxml.internal.CfgXmlAccessServiceImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.boot.internal.DefaultSessionFactoryBuilderService", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.bytecode.enhance.spi.interceptor.BytecodeInterceptorLogging_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.bytecode.enhance.spi.interceptor.BytecodeInterceptorLogging_$logger_en" +}, +{ + "name":"org.hibernate.bytecode.enhance.spi.interceptor.BytecodeInterceptorLogging_$logger_en_US" +}, +{ + "name":"org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.cache.internal.DisabledCaching", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.cache.internal.NoCachingRegionFactory", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.cfg.beanvalidation.TypeSafeActivator", + "methods":[{"name":"activate","parameterTypes":["org.hibernate.cfg.beanvalidation.ActivationContext"] }] +}, +{ + "name":"org.hibernate.engine.config.internal.ConfigurationServiceImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.engine.jdbc.batch.internal.BatchBuilderImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl", + "queryAllPublicMethods":true, + "methods":[{"name":"setJndiService","parameterTypes":["org.hibernate.engine.jndi.spi.JndiService"] }] +}, +{ + "name":"org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.engine.jdbc.dialect.internal.DialectResolverSet", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.engine.jdbc.internal.JdbcServicesImpl", + "queryAllPublicMethods":true +}, + { + "name": "org.hibernate.engine.jndi.internal.JndiServiceImpl", + "queryAllPublicMethods": true + }, + { + "name": "org.hibernate.engine.query.internal.NativeQueryInterpreterStandardImpl", + "queryAllPublicMethods": true + }, + { + "name": "org.hibernate.engine.spi.PrimeAmongSecondarySupertypes", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "org.hibernate.engine.spi.SessionImplementor" + }, + { + "name": "org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform", + "queryAllPublicMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.hibernate.event.spi.EventType", + "allDeclaredFields": true +}, +{ + "name":"org.hibernate.id.Assigned", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.id.enhanced.NoopOptimizer", + "methods":[{"name":"","parameterTypes":["java.lang.Class","int"] }] +}, +{ + "name":"org.hibernate.id.enhanced.PooledOptimizer", + "methods":[{"name":"","parameterTypes":["java.lang.Class","int"] }] +}, +{ + "name":"org.hibernate.id.enhanced.SequenceStyleGenerator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.id.enhanced.StandardNamingStrategy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.internal.CoreMessageLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.internal.CoreMessageLogger_$logger_en" +}, +{ + "name":"org.hibernate.internal.CoreMessageLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.internal.EntityManagerMessageLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.internal.EntityManagerMessageLogger_$logger_en" +}, +{ + "name":"org.hibernate.internal.EntityManagerMessageLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.internal.log.DeprecationLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.internal.log.DeprecationLogger_$logger_en" +}, +{ + "name":"org.hibernate.internal.log.DeprecationLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.metamodel.mapping.MappingModelCreationLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.metamodel.mapping.MappingModelCreationLogger_$logger_en" +}, +{ + "name":"org.hibernate.metamodel.mapping.MappingModelCreationLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.metamodel.model.domain.JpaMetamodel" +}, +{ + "name":"org.hibernate.persister.collection.BasicCollectionPersister", + "methods":[{"name":"","parameterTypes":["org.hibernate.mapping.Collection","org.hibernate.cache.spi.access.CollectionDataAccess","org.hibernate.metamodel.spi.RuntimeModelCreationContext"] }] +}, +{ + "name":"org.hibernate.persister.entity.SingleTableEntityPersister", + "methods":[{"name":"","parameterTypes":["org.hibernate.mapping.PersistentClass","org.hibernate.cache.spi.access.EntityDataAccess","org.hibernate.cache.spi.access.NaturalIdDataAccess","org.hibernate.metamodel.spi.RuntimeModelCreationContext"] }] +}, +{ + "name":"org.hibernate.persister.internal.PersisterFactoryImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.persister.internal.StandardPersisterClassResolver", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.property.access.internal.PropertyAccessStrategyResolverStandardImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.proxy.HibernateProxy", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "getHibernateLazyInitializer", + "parameterTypes": [] + }, + { + "name": "writeReplace", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.hibernate.proxy.ProxyConfiguration", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.hibernate.query.Query", + "methods": [ + { + "name": "getResultList", + "parameterTypes": [] + }, + { + "name": "getSingleResult", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.hibernate.query.QueryLogging_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.query.QueryLogging_$logger_en" +}, +{ + "name":"org.hibernate.query.QueryLogging_$logger_en_US" +}, +{ + "name":"org.hibernate.query.QueryProducer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "createMutationQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaDelete" + ] + }, + { + "name": "createMutationQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaUpdate" + ] + }, + { + "name": "createMutationQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createMutationQuery", + "parameterTypes": [ + "org.hibernate.query.criteria.JpaCriteriaInsertSelect" + ] + }, + { + "name": "createNamedMutationQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNamedSelectionQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNamedSelectionQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "createNativeMutationQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "createNativeQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "createSelectionQuery", + "parameterTypes": [ + "jakarta.persistence.criteria.CriteriaQuery" + ] + }, + { + "name": "createSelectionQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "createSelectionQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "getNamedNativeQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getNamedNativeQuery", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "getNamedQuery", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.hibernate.query.TypedParameterValue" +}, +{ + "name":"org.hibernate.query.hql.HqlLogging_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.query.hql.HqlLogging_$logger_en" +}, +{ + "name":"org.hibernate.query.hql.HqlLogging_$logger_en_US" +}, +{ + "name":"org.hibernate.resource.beans.container.spi.BeanContainer" +}, +{ + "name":"org.hibernate.resource.beans.internal.ManagedBeanRegistryImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorBuilderImpl", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.service.internal.SessionFactoryServiceRegistryFactoryImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.sql.ast.tree.SqlAstTreeLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.sql.ast.tree.SqlAstTreeLogger_$logger_en" +}, +{ + "name":"org.hibernate.sql.ast.tree.SqlAstTreeLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.sql.exec.SqlExecLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.sql.exec.SqlExecLogger_$logger_en" +}, +{ + "name":"org.hibernate.sql.exec.SqlExecLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.sql.results.LoadingLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.sql.results.LoadingLogger_$logger_en" +}, +{ + "name":"org.hibernate.sql.results.LoadingLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.sql.results.ResultsLogger_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.sql.results.ResultsLogger_$logger_en" +}, +{ + "name":"org.hibernate.sql.results.ResultsLogger_$logger_en_US" +}, +{ + "name":"org.hibernate.stat.HibernateMetrics" +}, +{ + "name":"org.hibernate.stat.internal.StatisticsImpl", + "queryAllPublicMethods":true +}, +{ + "name":"org.hibernate.type.SqlTypes", + "allPublicFields":true +}, +{ + "name":"org.hibernate.validator.internal.constraintvalidators.bv.NotNullValidator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.validator.internal.constraintvalidators.bv.notempty.NotEmptyValidatorForCharSequence", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.validator.internal.engine.AbstractConfigurationImpl", + "methods":[{"name":"externalClassLoader","parameterTypes":["java.lang.ClassLoader"] }] +}, +{ + "name":"org.hibernate.validator.internal.engine.ConfigurationImpl" +}, +{ + "name":"org.hibernate.validator.internal.engine.resolver.JPATraversableResolver", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.hibernate.validator.internal.util.logging.Log_$logger", + "methods":[{"name":"","parameterTypes":["org.jboss.logging.Logger"] }] +}, +{ + "name":"org.hibernate.validator.internal.util.logging.Log_$logger_en" +}, +{ + "name":"org.hibernate.validator.internal.util.logging.Log_$logger_en_US" +}, +{ + "name":"org.hibernate.validator.internal.util.logging.Messages_$bundle", + "fields":[{"name":"INSTANCE"}] +}, + { + "name": "org.hibernate.validator.internal.util.logging.Messages_$bundle_en" + }, + { + "name": "org.hibernate.validator.internal.util.logging.Messages_$bundle_en_US" + }, + { + "name": "org.influxdb.InfluxDB" + }, + { + "name": "org.javers.core.JaversCore", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.diff.DiffFactory", + "org.javers.core.metamodel.type.TypeMapper", + "org.javers.core.json.JsonConverter", + "org.javers.core.commit.CommitFactory", + "org.javers.repository.api.JaversExtendedRepository", + "org.javers.repository.jql.QueryRunner", + "org.javers.core.metamodel.object.GlobalIdFactory", + "org.javers.core.CoreConfiguration" + ] + } + ] + }, + { + "name": "org.javers.core.commit.CommitFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.diff.DiffFactory", + "org.javers.repository.api.JaversExtendedRepository", + "org.javers.common.date.DateProvider", + "org.javers.core.graph.LiveGraphFactory", + "org.javers.core.snapshot.SnapshotFactory", + "org.javers.core.snapshot.SnapshotGraphFactory", + "org.javers.core.snapshot.ChangedCdoSnapshotsFactory", + "org.javers.core.commit.CommitIdFactory" + ] + } + ] + }, + { + "name": "org.javers.core.commit.CommitIdFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.CoreConfiguration", + "org.javers.repository.api.JaversExtendedRepository", + "org.javers.core.commit.CommitSeqGenerator", + "org.javers.core.commit.DistributedCommitSeqGenerator" + ] + } + ] + }, + { + "name": "org.javers.core.commit.CommitSeqGenerator", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.commit.DistributedCommitSeqGenerator", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.diff.DiffFactory", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper", + "java.util.List", + "java.util.List", + "org.javers.core.graph.LiveGraphFactory", + "org.javers.core.CoreConfiguration" + ] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.ArrayChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.diff.appenders.MapChangeAppender" + ] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.CollectionAsListChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.diff.appenders.MapChangeAppender" + ] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.MapChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.NewObjectAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.ObjectRemovedAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.OptionalChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.ReferenceChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.SetChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.SimpleListChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.diff.appenders.MapChangeAppender" + ] + } + ] + }, + { + "name": "org.javers.core.diff.appenders.ValueChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.graph.CollectionsCdoFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.scanner.ClassScanner", + "org.javers.core.graph.TailoredJaversMemberFactory", + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.graph.LiveCdoFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.object.GlobalIdFactory", + "org.javers.core.graph.ObjectAccessHook", + "org.javers.core.metamodel.type.TypeMapper", + "org.javers.core.graph.ObjectHasher" + ] + } + ] + }, + { + "name": "org.javers.core.graph.LiveGraphFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper", + "org.javers.core.graph.LiveCdoFactory", + "org.javers.core.graph.CollectionsCdoFactory" + ] + } + ] + }, + { + "name": "org.javers.core.graph.ObjectAccessHookDoNothingImpl", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.graph.ObjectHasher", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.snapshot.SnapshotFactory", + "org.javers.core.json.JsonConverter" + ] + } + ] + }, + { + "name": "org.javers.core.graph.TailoredJaversFieldFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.json.JsonConverterBuilder", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.ArrayChangeTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.ChangeTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.ListChangeTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.MapChangeTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.NewObjectTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.ObjectRemovedTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.ReferenceChangeTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.SetChangeTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.change.ValueChangeTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.commit.CdoSnapshotStateTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.commit.CdoSnapshotTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.commit.CommitIdTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.commit.CommitMetadataTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.commit.GlobalIdTypeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.json.typeadapter.commit.JsonElementFakeAdapter", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.metamodel.annotation.DiffIgnore", + "queryAllDeclaredMethods": true + }, + { + "name": "org.javers.core.metamodel.object.GlobalIdFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper", + "org.javers.core.graph.ObjectAccessHook" + ] + } + ] + }, + { + "name": "org.javers.core.metamodel.scanner.AnnotationNamesProvider", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.core.metamodel.scanner.ClassAnnotationsScanner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.scanner.AnnotationNamesProvider" + ] + } + ] + }, + { + "name": "org.javers.core.metamodel.scanner.ClassScanner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.scanner.PropertyScanner", + "org.javers.core.metamodel.scanner.ClassAnnotationsScanner" + ] + } + ] + }, + { + "name": "org.javers.core.metamodel.scanner.FieldBasedPropertyScanner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.scanner.AnnotationNamesProvider" + ] + } + ] + }, + { + "name": "org.javers.core.metamodel.type.ListType", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.reflect.Type", + "org.javers.core.metamodel.type.TypeMapperLazy" + ] + } + ] + }, + { + "name": "org.javers.core.metamodel.type.MapType", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.reflect.Type", + "org.javers.core.metamodel.type.TypeMapperLazy" + ] + } + ] + }, + { + "name": "org.javers.core.metamodel.type.PrimitiveType", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.reflect.Type" + ] + } + ] + }, + { + "name": "org.javers.core.metamodel.type.TypeMapper", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.scanner.ClassScanner", + "org.javers.core.CoreConfiguration", + "org.javers.core.metamodel.type.DynamicMappingStrategy" + ] + } + ] + }, + { + "name": "org.javers.core.snapshot.ChangedCdoSnapshotsFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.snapshot.SnapshotFactory" + ] + } + ] + }, + { + "name": "org.javers.core.snapshot.SnapshotDiffer", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.diff.DiffFactory", + "org.javers.core.CoreConfiguration" + ] + } + ] + }, + { + "name": "org.javers.core.snapshot.SnapshotFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.core.snapshot.SnapshotGraphFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.repository.api.JaversExtendedRepository" + ] + } + ] + }, + { + "name": "org.javers.guava.MultimapChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.javers.guava.MultisetChangeAppender", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.javers.repository.api.JaversExtendedRepository", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.repository.api.JaversRepository", + "org.javers.core.snapshot.SnapshotDiffer" + ] + } + ] + }, + { + "name": "org.javers.repository.jql.ChangesQueryRunner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.repository.jql.QueryCompiler", + "org.javers.repository.api.JaversExtendedRepository" + ] + } + ] + }, + { + "name": "org.javers.repository.jql.QueryCompiler", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.metamodel.object.GlobalIdFactory", + "org.javers.core.metamodel.type.TypeMapper", + "org.javers.core.CoreConfiguration" + ] + } + ] + }, + { + "name": "org.javers.repository.jql.QueryRunner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.repository.jql.ChangesQueryRunner", + "org.javers.repository.jql.SnapshotQueryRunner", + "org.javers.repository.jql.ShadowStreamQueryRunner" + ] + } + ] + }, + { + "name": "org.javers.repository.jql.ShadowQueryRunner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.repository.jql.SnapshotQueryRunner", + "org.javers.repository.api.JaversExtendedRepository", + "org.javers.shadow.ShadowFactory", + "org.javers.core.CoreConfiguration" + ] + } + ] + }, + { + "name": "org.javers.repository.jql.ShadowStreamQueryRunner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.repository.jql.ShadowQueryRunner", + "org.javers.repository.jql.QueryCompiler" + ] + } + ] + }, + { + "name": "org.javers.repository.jql.SnapshotQueryRunner", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.repository.jql.QueryCompiler", + "org.javers.core.metamodel.object.GlobalIdFactory", + "org.javers.repository.api.JaversExtendedRepository" + ] + } + ] + }, + { + "name": "org.javers.shadow.ShadowFactory", + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.javers.core.json.JsonConverter", + "org.javers.core.metamodel.type.TypeMapper" + ] + } + ] + }, + { + "name": "org.jboss.logging.Logger" + }, + { + "name": "org.jmolecules.ddd.annotation$Identity" + }, + { + "name": "org.jmolecules.ddd.annotation.Identity" + }, + { + "name": "org.jmolecules.ddd.types$Association" + }, + { + "name": "org.jmolecules.ddd.types.Association" + }, + { + "name": "org.joda.time.LocalDate" + }, + { + "name": "org.joda.time.ReadableInstant" + }, + { + "name": "org.jooq.DSLContext" + }, + { + "name": "org.locationtech.jts.geom.Geometry" + }, + { + "name": "org.neo4j.driver.Driver" +}, +{ + "name":"org.nzbhydra.DevEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.DevEndpoint$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.GenericResponse", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "getMessage", + "parameterTypes": [] + }, + { + "name": "isSuccessful", + "parameterTypes": [] + }, + { + "name": "", + "parameterTypes": [ + "java.lang.Boolean", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.InstanceCounter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "downloadInstanceCounter", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.NzbHydra", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "getCacheManager", + "parameterTypes": [] + }, + { + "name": "loadBaseConfig", + "parameterTypes": [] + }, + { + "name": "main", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "startupDone", + "parameterTypes": [ + "org.springframework.boot.context.event.ApplicationReadyEvent" + ] + }, + { + "name": "warnIfSettingsOverwritten", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.NzbHydraNativeEntrypoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "main", + "parameterTypes": [ + "java.lang.String[]" + ] + } + ] +}, +{ + "name":"org.nzbhydra.NzbHydraNativeEntrypoint$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] +}, +{ + "name":"org.nzbhydra.api.CapsGenerator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.api.CategoryConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "convertToDatabaseColumn", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "convertToEntityAttribute", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setCategoryProvider", + "parameterTypes": [ + "org.nzbhydra.searching.CategoryProvider" + ] + } + ] +}, + { + "name": "org.nzbhydra.api.ExternalApi", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "api", + "parameterTypes": [ + "org.nzbhydra.mapping.newznab.NewznabParameters", + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "handler", + "parameterTypes": [ + "org.nzbhydra.api.ExternalApiException" + ] + } + ] + }, +{ + "name":"org.nzbhydra.api.MockSearch", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.api.NewznabJsonTransformer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.api.NewznabXmlTransformer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.api.stats.ExternalApiStats", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.auth.AsyncSupportFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.auth.AuthAndAccessEventHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "handle", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "org.springframework.security.access.AccessDeniedException" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.AuthWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.auth.HydraAnonymousAuthenticationFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.nzbhydra.config.ConfigProvider" + ] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "doFilter", + "parameterTypes": [ + "jakarta.servlet.ServletRequest", + "jakarta.servlet.ServletResponse", + "jakarta.servlet.FilterChain" + ] + }, + { + "name": "getAuthorities", + "parameterTypes": [] + }, + { + "name": "getPrincipal", + "parameterTypes": [] + }, + { + "name": "handleConfigChangedEvent", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + }, + { + "name": "setAuthenticationDetailsSource", + "parameterTypes": [ + "org.springframework.security.authentication.AuthenticationDetailsSource" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.HydraEmbeddedServletContainer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "customize", + "parameterTypes": [ + "org.springframework.boot.web.server.WebServerFactory" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.HydraGlobalMethodSecurityConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "methodSecurityMetadataSource", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.auth.HydraGlobalMethodSecurityConfiguration$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "methodSecurityInterceptor", + "parameterTypes": [ + "org.springframework.security.access.method.MethodSecurityMetadataSource" + ] + }, + { + "name": "methodSecurityMetadataSource", + "parameterTypes": [] + }, + { + "name": "preInvocationAuthorizationAdvice", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.HydraGlobalMethodSecurityConfiguration$$SpringCGLIB$$1", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.auth.HydraGlobalMethodSecurityConfiguration$$SpringCGLIB$$2", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.auth.HydraUserDetailsManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig" + ] + }, + { + "name": "changePassword", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "createUser", + "parameterTypes": [ + "org.springframework.security.core.userdetails.UserDetails" + ] + }, + { + "name": "deleteUser", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "handleConfigChangedEvent", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + }, + { + "name": "loadUserByUsername", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "updateUser", + "parameterTypes": [ + "org.springframework.security.core.userdetails.UserDetails" + ] + }, + { + "name": "userExists", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.LoginAndAccessAttemptService", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.auth.PersistentLoginsEntity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getSeries", + "parameterTypes": [] + }, + { + "name": "setSeries", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.PersistentLoginsEntity_" +}, +{ + "name":"org.nzbhydra.auth.SecurityConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "authManager", + "parameterTypes": [ + "org.springframework.security.config.annotation.web.builders.HttpSecurity" + ] + }, + { + "name": "defaultHttpFirewall", + "parameterTypes": [] + }, + { + "name": "filterChain", + "parameterTypes": [ + "org.springframework.security.config.annotation.web.builders.HttpSecurity", + "org.springframework.security.authentication.AuthenticationManager" + ] + }, + { + "name": "handleNewConfig", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.SecurityConfig$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "authManager", + "parameterTypes": [ + "org.springframework.security.config.annotation.web.builders.HttpSecurity" + ] + }, + { + "name": "defaultHttpFirewall", + "parameterTypes": [] + }, + { + "name": "filterChain", + "parameterTypes": [ + "org.springframework.security.config.annotation.web.builders.HttpSecurity", + "org.springframework.security.authentication.AuthenticationManager" + ] + } + ] +}, +{ + "name":"org.nzbhydra.auth.SecurityConfig$$SpringCGLIB$$1", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.auth.SecurityConfig$$SpringCGLIB$$2", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.auth.UserInfosProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.backup.BackupAndRestore", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "init", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.backup.BackupAndRestore$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, + { + "name":"org.nzbhydra.backup.BackupTask", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "createBackup", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.backup.BackupWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.backup.BackupWeb$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods": [ + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] +}, + { + "name": "org.nzbhydra.config.BaseConfig", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getAuth", + "parameterTypes": [] + }, + { + "name": "getCategoriesConfig", + "parameterTypes": [] + }, + { + "name": "getDownloading", + "parameterTypes": [] + }, + { + "name": "getGenericStorage", + "parameterTypes": [] + }, + { + "name": "getIndexers", + "parameterTypes": [] + }, + { + "name": "getMain", + "parameterTypes": [] + }, + { + "name": "getNotificationConfig", + "parameterTypes": [] + }, + { + "name": "getSearching", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "setAuth", + "parameterTypes": [ + "org.nzbhydra.config.auth.AuthConfig" + ] + }, + { + "name": "setCategoriesConfig", + "parameterTypes": [ + "org.nzbhydra.config.category.CategoriesConfig" + ] + }, + { + "name": "setDownloading", + "parameterTypes": [ + "org.nzbhydra.config.downloading.DownloadingConfig" + ] + }, + { + "name": "setGenericStorage", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setIndexers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setMain", + "parameterTypes": [ + "org.nzbhydra.config.MainConfig" + ] + }, + { + "name": "setNotificationConfig", + "parameterTypes": [ + "org.nzbhydra.config.NotificationConfig" + ] + }, + { + "name": "setSearching", + "parameterTypes": [ + "org.nzbhydra.config.SearchingConfig" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.config.BaseConfigHandler", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "init", + "parameterTypes": [] + }, + { + "name": "onShutdown", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.config.ConfigProvider", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "handleNewConfig", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + } + ] + }, +{ + "name":"org.nzbhydra.config.ConfigSpringConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "categoriesConfig", + "parameterTypes": [] + }, + { + "name": "downloaderConfig", + "parameterTypes": [] + }, + { + "name": "mainConfig", + "parameterTypes": [] + }, + { + "name": "searchingConfig", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.config.ConfigSpringConfiguration$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.ConfigSpringConfiguration$$SpringCGLIB$$1", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.config.ConfigSpringConfiguration$$SpringCGLIB$$2", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.config.ConfigWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getConfig", + "parameterTypes": [ + "jakarta.servlet.http.HttpSession" + ] + }, + { + "name": "setConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.FileSystemBrowser", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.config.HistoryUserInfoType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.nzbhydra.config.LoggingConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getConsolelevel", + "parameterTypes": [] + }, + { + "name": "getHistoryUserInfoType", + "parameterTypes": [] + }, + { + "name": "getLogMaxHistory", + "parameterTypes": [] + }, + { + "name": "getLogfilelevel", + "parameterTypes": [] + }, + { + "name": "getMarkersToLog", + "parameterTypes": [] + }, + { + "name": "isLogGc", + "parameterTypes": [] + }, + { + "name": "isLogIpAddresses", + "parameterTypes": [] + }, + { + "name": "isLogUsername", + "parameterTypes": [] + }, + { + "name": "isMapIpToHost", + "parameterTypes": [] + }, + { + "name": "setConsolelevel", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setHistoryUserInfoType", + "parameterTypes": [ + "org.nzbhydra.config.HistoryUserInfoType" + ] + }, + { + "name": "setLogGc", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setLogIpAddresses", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setLogMaxHistory", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setLogUsername", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setLogfilelevel", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setMapIpToHost", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setMarkersToLog", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.MainConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getApiKey", + "parameterTypes": [] + }, + { + "name": "getBackupEveryXDays", + "parameterTypes": [] + }, + { + "name": "getBackupFolder", + "parameterTypes": [] + }, + { + "name": "getConfigVersion", + "parameterTypes": [] + }, + { + "name": "getDatabaseCompactTime", + "parameterTypes": [] + }, + { + "name": "getDatabaseRetentionTime", + "parameterTypes": [] + }, + { + "name": "getDatabaseWriteDelay", + "parameterTypes": [] + }, + { + "name": "getDeleteBackupsAfterWeeks", + "parameterTypes": [] + }, + { + "name": "getDereferer", + "parameterTypes": [] + }, + { + "name": "getHost", + "parameterTypes": [] + }, + { + "name": "getKeepHistoryForWeeks", + "parameterTypes": [] + }, + { + "name": "getKeepStatsForWeeks", + "parameterTypes": [] + }, + { + "name": "getLogging", + "parameterTypes": [] + }, + { + "name": "getPort", + "parameterTypes": [] + }, + { + "name": "getProxyHost", + "parameterTypes": [] + }, + { + "name": "getProxyIgnoreDomains", + "parameterTypes": [] + }, + { + "name": "getProxyPassword", + "parameterTypes": [] + }, + { + "name": "getProxyPort", + "parameterTypes": [] + }, + { + "name": "getProxyType", + "parameterTypes": [] + }, + { + "name": "getProxyUsername", + "parameterTypes": [] + }, + { + "name": "getRepositoryBase", + "parameterTypes": [] + }, + { + "name": "getSniDisabledFor", + "parameterTypes": [] + }, + { + "name": "getSslKeyStore", + "parameterTypes": [] + }, + { + "name": "getSslKeyStorePassword", + "parameterTypes": [] + }, + { + "name": "getTheme", + "parameterTypes": [] + }, + { + "name": "getUrlBase", + "parameterTypes": [] + }, + { + "name": "getVerifySslDisabledFor", + "parameterTypes": [] + }, + { + "name": "getXmx", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isBackupBeforeUpdate", + "parameterTypes": [] + }, + { + "name": "isDisableSslLocally", + "parameterTypes": [] + }, + { + "name": "isInstanceCounterDownloaded", + "parameterTypes": [] + }, + { + "name": "isKeepHistory", + "parameterTypes": [] + }, + { + "name": "isProxyIgnoreLocal", + "parameterTypes": [] + }, + { + "name": "isShowNews", + "parameterTypes": [] + }, + { + "name": "isShowUpdateBannerOnUpdatedExternally", + "parameterTypes": [] + }, + { + "name": "isShowWhatsNewBanner", + "parameterTypes": [] + }, + { + "name": "isShutdownForRestart", + "parameterTypes": [] + }, + { + "name": "isSsl", + "parameterTypes": [] + }, + { + "name": "isStartupBrowser", + "parameterTypes": [] + }, + { + "name": "isUpdateAutomatically", + "parameterTypes": [] + }, + { + "name": "isUpdateCheckEnabled", + "parameterTypes": [] + }, + { + "name": "isUpdateToPrereleases", + "parameterTypes": [] + }, + { + "name": "isUseCsrf", + "parameterTypes": [] + }, + { + "name": "isVerifySsl", + "parameterTypes": [] + }, + { + "name": "isWelcomeShown", + "parameterTypes": [] + }, + { + "name": "setApiKey", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setBackupBeforeUpdate", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setBackupEveryXDays", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setBackupFolder", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConfigVersion", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setDatabaseCompactTime", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setDatabaseRetentionTime", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setDatabaseWriteDelay", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setDeleteBackupsAfterWeeks", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setDereferer", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDisableSslLocally", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setHost", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setInstanceCounterDownloaded", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setKeepHistory", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setKeepHistoryForWeeks", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setKeepStatsForWeeks", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setLogging", + "parameterTypes": [ + "org.nzbhydra.config.LoggingConfig" + ] + }, + { + "name": "setPort", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setProxyHost", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setProxyIgnoreDomains", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setProxyIgnoreLocal", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setProxyPassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setProxyPort", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setProxyType", + "parameterTypes": [ + "org.nzbhydra.config.ProxyType" + ] + }, + { + "name": "setProxyUsername", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setRepositoryBase", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setShowNews", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setShowUpdateBannerOnUpdatedExternally", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setShowWhatsNewBanner", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setShutdownForRestart", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setSniDisabledFor", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setSsl", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setSslKeyStore", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSslKeyStorePassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setStartupBrowser", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setTheme", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUpdateAutomatically", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUpdateCheckEnabled", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUpdateToPrereleases", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUrlBase", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUseCsrf", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setVerifySsl", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setVerifySslDisabledFor", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setWelcomeShown", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setXmx", + "parameterTypes": [ + "int" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.config.NotificationConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getAppriseApiUrl", + "parameterTypes": [] + }, + { + "name": "getAppriseCliPath", + "parameterTypes": [] + }, + { + "name": "getAppriseType", + "parameterTypes": [] + }, + { + "name": "getDisplayNotificationsMax", + "parameterTypes": [] + }, + { + "name": "getEntries", + "parameterTypes": [] + }, + { + "name": "getFilterOuts", + "parameterTypes": [] + }, + { + "name": "isDisplayNotifications", + "parameterTypes": [] + }, + { + "name": "setAppriseApiUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setAppriseCliPath", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setAppriseType", + "parameterTypes": [ + "org.nzbhydra.config.NotificationConfig$AppriseType" + ] + }, + { + "name": "setDisplayNotifications", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setDisplayNotificationsMax", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setEntries", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setFilterOuts", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.NotificationConfig$AppriseType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, + { + "name": "org.nzbhydra.config.NotificationConfigEntry", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "org.nzbhydra.config.NotificationConfigEntry$MessageType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.ProxyType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.RestartRequired", + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.SearchSourceRestriction", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, +{ + "name":"org.nzbhydra.config.SearchingConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getAlwaysConvertIds", + "parameterTypes": [] + }, + { + "name": "getApplyRestrictions", + "parameterTypes": [] + }, + { + "name": "getCoverSize", + "parameterTypes": [] + }, + { + "name": "getCustomMappings", + "parameterTypes": [] + }, + { + "name": "getCustomQuickFilterButtons", + "parameterTypes": [] + }, + { + "name": "getDuplicateAgeThreshold", + "parameterTypes": [] + }, + { + "name": "getDuplicateSizeThresholdInPercent", + "parameterTypes": [] + }, + { + "name": "getForbiddenGroups", + "parameterTypes": [] + }, + { + "name": "getForbiddenPosters", + "parameterTypes": [] + }, + { + "name": "getForbiddenRegex", + "parameterTypes": [] + }, + { + "name": "getForbiddenWords", + "parameterTypes": [] + }, + { + "name": "getGenerateQueries", + "parameterTypes": [] + }, + { + "name": "getGenerateQueriesFormat", + "parameterTypes": [] + }, + { + "name": "getGlobalCacheTimeMinutes", + "parameterTypes": [] + }, + { + "name": "getIdFallbackToQueryGeneration", + "parameterTypes": [] + }, + { + "name": "getKeepSearchResultsForDays", + "parameterTypes": [] + }, + { + "name": "getLanguage", + "parameterTypes": [] + }, + { + "name": "getLanguagesToKeep", + "parameterTypes": [] + }, + { + "name": "getLoadLimitInternal", + "parameterTypes": [] + }, + { + "name": "getMaxAge", + "parameterTypes": [] + }, + { + "name": "getMinSeeders", + "parameterTypes": [] + }, + { + "name": "getPreselectQuickFilterButtons", + "parameterTypes": [] + }, + { + "name": "getRemoveTrailing", + "parameterTypes": [] + }, + { + "name": "getRequiredRegex", + "parameterTypes": [] + }, + { + "name": "getRequiredWords", + "parameterTypes": [] + }, + { + "name": "getTimeout", + "parameterTypes": [] + }, + { + "name": "getUserAgent", + "parameterTypes": [] + }, + { + "name": "getUserAgents", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isAlwaysShowQuickFilterButtons", + "parameterTypes": [] + }, + { + "name": "isIgnoreLoadLimitingForInternalSearches", + "parameterTypes": [] + }, + { + "name": "isIgnorePassworded", + "parameterTypes": [] + }, + { + "name": "isIgnoreTemporarilyDisabled", + "parameterTypes": [] + }, + { + "name": "isLoadAllCachedOnInternal", + "parameterTypes": [] + }, + { + "name": "isReplaceUmlauts", + "parameterTypes": [] + }, + { + "name": "isSendTorznabCategories", + "parameterTypes": [] + }, + { + "name": "isShowQuickFilterButtons", + "parameterTypes": [] + }, + { + "name": "isTransformNewznabCategories", + "parameterTypes": [] + }, + { + "name": "isUseOriginalCategories", + "parameterTypes": [] + }, + { + "name": "isWrapApiErrors", + "parameterTypes": [] + }, + { + "name": "setAlwaysConvertIds", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setAlwaysShowQuickFilterButtons", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setApplyRestrictions", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setCoverSize", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setCustomMappings", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setCustomQuickFilterButtons", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setDuplicateAgeThreshold", + "parameterTypes": [ + "float" + ] + }, + { + "name": "setDuplicateSizeThresholdInPercent", + "parameterTypes": [ + "float" + ] + }, + { + "name": "setForbiddenGroups", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setForbiddenPosters", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setForbiddenRegex", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setForbiddenWords", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setGenerateQueries", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setGenerateQueriesFormat", + "parameterTypes": [ + "org.nzbhydra.config.indexer.QueryFormat" + ] + }, + { + "name": "setGlobalCacheTimeMinutes", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setIdFallbackToQueryGeneration", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setIgnoreLoadLimitingForInternalSearches", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setIgnorePassworded", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setIgnoreTemporarilyDisabled", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setKeepSearchResultsForDays", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setLanguage", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setLanguagesToKeep", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setLoadAllCachedOnInternal", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setLoadLimitInternal", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setMaxAge", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setMinSeeders", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setPreselectQuickFilterButtons", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setRemoveTrailing", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setReplaceUmlauts", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRequiredRegex", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setRequiredWords", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setSendTorznabCategories", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setShowQuickFilterButtons", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setTimeout", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setTransformNewznabCategories", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUseOriginalCategories", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUserAgent", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUserAgents", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setWrapApiErrors", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.config.auth.AuthConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getAuthHeader", + "parameterTypes": [] + }, + { + "name": "getAuthHeaderIpRanges", + "parameterTypes": [] + }, + { + "name": "getAuthType", + "parameterTypes": [] + }, + { + "name": "getRememberMeValidityDays", + "parameterTypes": [] + }, + { + "name": "getUsers", + "parameterTypes": [] + }, + { + "name": "isAllowApiStats", + "parameterTypes": [] + }, + { + "name": "isRememberUsers", + "parameterTypes": [] + }, + { + "name": "isRestrictAdmin", + "parameterTypes": [] + }, + { + "name": "isRestrictDetailsDl", + "parameterTypes": [] + }, + { + "name": "isRestrictIndexerSelection", + "parameterTypes": [] + }, + { + "name": "isRestrictSearch", + "parameterTypes": [] + }, + { + "name": "isRestrictStats", + "parameterTypes": [] + }, + { + "name": "setAllowApiStats", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setAuthHeader", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setAuthHeaderIpRanges", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setAuthType", + "parameterTypes": [ + "org.nzbhydra.config.auth.AuthType" + ] + }, + { + "name": "setRememberMeValidityDays", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setRememberUsers", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRestrictAdmin", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRestrictDetailsDl", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRestrictIndexerSelection", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRestrictSearch", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRestrictStats", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUsers", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.auth.AuthType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.nzbhydra.config.auth.UserAuthConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors": true +}, +{ + "name":"org.nzbhydra.config.category.CategoriesConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getCategories", + "parameterTypes": [] + }, + { + "name": "getDefaultCategory", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isEnableCategorySizes", + "parameterTypes": [] + }, + { + "name": "setCategories", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setDefaultCategory", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setEnableCategorySizes", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.config.category.Category", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getApplyRestrictionsType", + "parameterTypes": [] + }, + { + "name": "getDescription", + "parameterTypes": [] + }, + { + "name": "getForbiddenRegex", + "parameterTypes": [] + }, + { + "name": "getForbiddenWords", + "parameterTypes": [] + }, + { + "name": "getIgnoreResultsFrom", + "parameterTypes": [] + }, + { + "name": "getMaxSizePreset", + "parameterTypes": [] + }, + { + "name": "getMinSizePreset", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getNewznabCategories", + "parameterTypes": [] + }, + { + "name": "getRequiredRegex", + "parameterTypes": [] + }, + { + "name": "getRequiredWords", + "parameterTypes": [] + }, + { + "name": "getSearchType", + "parameterTypes": [] + }, + { + "name": "getSubtype", + "parameterTypes": [] + }, + { + "name": "isApplySizeLimitsToApi", + "parameterTypes": [] + }, + { + "name": "isMayBeSelected", + "parameterTypes": [] + }, + { + "name": "isPreselect", + "parameterTypes": [] + }, + { + "name": "setApplyRestrictionsType", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setApplySizeLimitsToApi", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setDescription", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setForbiddenRegex", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setForbiddenWords", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setIgnoreResultsFrom", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setMaxSizePreset", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setMayBeSelected", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setMinSizePreset", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setNewznabCategories", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setPreselect", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRequiredRegex", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setRequiredWords", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setSearchType", + "parameterTypes": [ + "org.nzbhydra.config.searching.SearchType" + ] + }, + { + "name": "setSubtype", + "parameterTypes": [ + "org.nzbhydra.config.category.Category$Subtype" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.category.Category$Subtype", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, + { + "name": "org.nzbhydra.config.category.NewznabCategoriesDeserializer", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "deserialize", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.databind.DeserializationContext" + ] + } + ] + }, +{ + "name":"org.nzbhydra.config.category.NewznabCategoriesSerializer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "serialize", + "parameterTypes": [ + "java.lang.Object", + "com.fasterxml.jackson.core.JsonGenerator", + "com.fasterxml.jackson.databind.SerializerProvider" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.downloading.DownloadType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.nzbhydra.config.downloading.DownloaderConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getApiKey", + "parameterTypes": [] + }, + { + "name": "getDefaultCategory", + "parameterTypes": [] + }, + { + "name": "getDownloadType", + "parameterTypes": [] + }, + { + "name": "getDownloaderType", + "parameterTypes": [] + }, + { + "name": "getIconCssClass", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getNzbAddingType", + "parameterTypes": [] + }, + { + "name": "getPassword", + "parameterTypes": [] + }, + { + "name": "getUrl", + "parameterTypes": [] + }, + { + "name": "getUsername", + "parameterTypes": [] + }, + { + "name": "isAddPaused", + "parameterTypes": [] + }, + { + "name": "isEnabled", + "parameterTypes": [] + }, + { + "name": "setAddPaused", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setApiKey", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDefaultCategory", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDownloadType", + "parameterTypes": [ + "org.nzbhydra.config.downloading.DownloadType" + ] + }, + { + "name": "setDownloaderType", + "parameterTypes": [ + "org.nzbhydra.downloading.DownloaderType" + ] + }, + { + "name": "setEnabled", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setIconCssClass", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setNzbAddingType", + "parameterTypes": [ + "org.nzbhydra.config.downloading.NzbAddingType" + ] + }, + { + "name": "setPassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUsername", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.downloading.DownloadingConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getDownloaders", + "parameterTypes": [] + }, + { + "name": "getExternalUrl", + "parameterTypes": [] + }, + { + "name": "getFallbackForFailed", + "parameterTypes": [] + }, + { + "name": "getNzbAccessType", + "parameterTypes": [] + }, + { + "name": "getPrimaryDownloader", + "parameterTypes": [] + }, + { + "name": "getSaveNzbsTo", + "parameterTypes": [] + }, + { + "name": "getSaveTorrentsTo", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isSendMagnetLinks", + "parameterTypes": [] + }, + { + "name": "isShowDownloaderStatus", + "parameterTypes": [] + }, + { + "name": "isUpdateStatuses", + "parameterTypes": [] + }, + { + "name": "setDownloaders", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setExternalUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setFallbackForFailed", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setNzbAccessType", + "parameterTypes": [ + "org.nzbhydra.config.downloading.FileDownloadAccessType" + ] + }, + { + "name": "setPrimaryDownloader", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSaveNzbsTo", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSaveTorrentsTo", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSendMagnetLinks", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setShowDownloaderStatus", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUpdateStatuses", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.config.downloading.FileDownloadAccessType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.nzbhydra.config.downloading.NzbAddingType", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, + { + "name": "org.nzbhydra.config.indexer.BackendType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, +{ + "name":"org.nzbhydra.config.indexer.IndexerCategoryConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getAnime", + "parameterTypes": [] + }, + { + "name": "getAudiobook", + "parameterTypes": [] + }, + { + "name": "getCategories", + "parameterTypes": [] + }, + { + "name": "getComic", + "parameterTypes": [] + }, + { + "name": "getEbook", + "parameterTypes": [] + }, + { + "name": "getMagazine", + "parameterTypes": [] + }, + { + "name": "setAnime", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setAudiobook", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setCategories", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setComic", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setEbook", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setMagazine", + "parameterTypes": [ + "java.lang.Integer" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.indexer.IndexerCategoryConfig$MainCategory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getSubCategories", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSubCategories", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.indexer.IndexerCategoryConfig$SubCategory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.config.indexer.IndexerConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getApiKey", + "parameterTypes": [] + }, + { + "name": "getApiPath", + "parameterTypes": [] + }, + { + "name": "getBackend", + "parameterTypes": [] + }, + { + "name": "getCategoryMapping", + "parameterTypes": [] + }, + { + "name": "getColor", + "parameterTypes": [] + }, + { + "name": "getCustomParameters", + "parameterTypes": [] + }, + { + "name": "getDisabledAt", + "parameterTypes": [] + }, + { + "name": "getDisabledLevel", + "parameterTypes": [] + }, + { + "name": "getDisabledUntil", + "parameterTypes": [] + }, + { + "name": "getDownloadLimit", + "parameterTypes": [] + }, + { + "name": "getEnabledCategories", + "parameterTypes": [] + }, + { + "name": "getEnabledForSearchSource", + "parameterTypes": [] + }, + { + "name": "getGeneralMinSize", + "parameterTypes": [] + }, + { + "name": "getHitLimit", + "parameterTypes": [] + }, + { + "name": "getHitLimitResetTime", + "parameterTypes": [] + }, + { + "name": "getHost", + "parameterTypes": [] + }, + { + "name": "getLastError", + "parameterTypes": [] + }, + { + "name": "getLoadLimitOnRandom", + "parameterTypes": [] + }, + { + "name": "getMinSeeders", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getPassword", + "parameterTypes": [] + }, + { + "name": "getSchedule", + "parameterTypes": [] + }, + { + "name": "getScore", + "parameterTypes": [] + }, + { + "name": "getSearchModuleType", + "parameterTypes": [] + }, + { + "name": "getState", + "parameterTypes": [] + }, + { + "name": "getSupportedSearchIds", + "parameterTypes": [] + }, + { + "name": "getSupportedSearchTypes", + "parameterTypes": [] + }, + { + "name": "getTimeout", + "parameterTypes": [] + }, + { + "name": "getUserAgent", + "parameterTypes": [] + }, + { + "name": "getUsername", + "parameterTypes": [] + }, + { + "name": "getVipExpirationDate", + "parameterTypes": [] + }, + { + "name": "isAllCapsChecked", + "parameterTypes": [] + }, + { + "name": "isConfigComplete", + "parameterTypes": [] + }, + { + "name": "isPreselect", + "parameterTypes": [] + }, + { + "name": "isShowOnSearch", + "parameterTypes": [] + }, + { + "name": "setAllCapsChecked", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setApiKey", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setApiPath", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setBackend", + "parameterTypes": [ + "org.nzbhydra.config.indexer.BackendType" + ] + }, + { + "name": "setCategoryMapping", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerCategoryConfig" + ] + }, + { + "name": "setColor", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConfigComplete", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setCustomParameters", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setDisabledAt", + "parameterTypes": [ + "java.time.Instant" + ] + }, + { + "name": "setDisabledLevel", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setDisabledUntil", + "parameterTypes": [ + "java.lang.Long" + ] + }, + { + "name": "setDownloadLimit", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setEnabledCategories", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setEnabledForSearchSource", + "parameterTypes": [ + "org.nzbhydra.config.SearchSourceRestriction" + ] + }, + { + "name": "setGeneralMinSize", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setHitLimit", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setHitLimitResetTime", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setHost", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setLastError", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setLoadLimitOnRandom", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setMinSeeders", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPreselect", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setSchedule", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setScore", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setSearchModuleType", + "parameterTypes": [ + "org.nzbhydra.config.indexer.SearchModuleType" + ] + }, + { + "name": "setShowOnSearch", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setState", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig$State" + ] + }, + { + "name": "setSupportedSearchIds", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setSupportedSearchTypes", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setTimeout", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setUserAgent", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUsername", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setVipExpirationDate", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, + { + "name": "org.nzbhydra.config.indexer.IndexerConfig$State", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.indexer.QueryFormat", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.indexer.SearchModuleType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.mediainfo.MediaIdType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.notification.NotificationEventType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.searching.AffectedValue", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.searching.CustomQueryAndTitleMapping", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "org.nzbhydra.config.searching.SearchType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.sensitive.SensitiveData", + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.config.validation.AuthConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "updateAfterLoading", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] +}, + { + "name": "org.nzbhydra.config.validation.BaseConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "initializeNewConfig", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "updateAfterLoading", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.CategoriesConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.ConfigValidationResult", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "getErrorMessages", + "parameterTypes": [] + }, + { + "name": "getNewConfig", + "parameterTypes": [] + }, + { + "name": "getWarningMessages", + "parameterTypes": [] + }, + { + "name": "isOk", + "parameterTypes": [] + }, + { + "name": "isRestartNeeded", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.ConfigValidator", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "methods": [ + { + "name": "initializeNewConfig", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "updateAfterLoading", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.DownloaderConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.DownloadingConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.IndexerConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.LoggingConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.MainConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "initializeNewConfig", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.NotificationConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.SearchingConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, + { + "name": "org.nzbhydra.config.validation.UserAuthConfigValidator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "doesValidate", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "prepareForSaving", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + }, + { + "name": "updateAfterLoading", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "validateConfig", + "parameterTypes": [ + "org.nzbhydra.config.BaseConfig", + "org.nzbhydra.config.BaseConfig", + "java.lang.Object" + ] + } + ] + }, +{ + "name":"org.nzbhydra.database.H2DialectExtended", + "methods":[{"name":"","parameterTypes":["org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo"] }] +}, +{ + "name":"org.nzbhydra.debuginfos.DebugInfosProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getDebugInfosAsZip", + "parameterTypes": [] + }, + { + "name": "logMetrics", + "parameterTypes": [] + }, + { + "name": "logThreadDump", + "parameterTypes": [] + } + ] +}, + { + "name": "org.nzbhydra.debuginfos.DebugInfosProvider$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods": [ + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] + }, + { + "name": "org.nzbhydra.debuginfos.DebugInfosProvider$DiffableCategoriesConfig", + "allDeclaredFields": true + }, + { + "name": "org.nzbhydra.debuginfos.DebugInfosWeb", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "createAndProvideDebugInfos", + "parameterTypes": [] + }, + { + "name": "downloadLogFile", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getCurrentLogFile", + "parameterTypes": [] + }, + { + "name": "getLogFilenames", + "parameterTypes": [] + }, + { + "name": "logThreadDump", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.downloading.AddFilesRequest", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCategory", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDownloaderName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSearchResults", + "parameterTypes": [ + "java.util.List" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.AddFilesRequest$SearchResult", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setMappedCategory", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setOriginalCategory", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSearchResultId", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.DownloadStatusUpdater", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "checkHistoryStatus", + "parameterTypes": [] + }, + { + "name": "checkQueueStatus", + "parameterTypes": [] + }, + { + "name": "onNzbDownloadEvent", + "parameterTypes": [ + "org.nzbhydra.downloading.FileDownloadEvent" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.DownloadStatusUpdater$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods": [ + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.DownloaderType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.downloading.FileDownloadEntity", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.downloading.FileDownloadEntity_" +}, +{ + "name":"org.nzbhydra.downloading.FileDownloadRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.downloading.FileDownloadStatus" +}, +{ + "name":"org.nzbhydra.downloading.FileHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getDownloadLinkForResults", + "parameterTypes": [ + "java.lang.Long", + "boolean", + "org.nzbhydra.searching.dtoseventsenums.SearchResultItem$DownloadType" + ] + }, + { + "name": "getDownloadLinkForSendingToDownloader", + "parameterTypes": [ + "java.lang.Long", + "boolean", + "org.nzbhydra.searching.dtoseventsenums.SearchResultItem$DownloadType" + ] + }, + { + "name": "getFileByGuid", + "parameterTypes": [ + "long", + "org.nzbhydra.searching.searchrequests.SearchSource" + ] + }, + { + "name": "getFilesAsZip", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "getTemporaryZipFiles", + "parameterTypes": [] + }, + { + "name": "saveNzbToBlackhole", + "parameterTypes": [ + "java.lang.Long" + ] + } + ] +}, + { + "name": "org.nzbhydra.downloading.FileHandler$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods": [ + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.FileZipResponse", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "getAddedIds", + "parameterTypes": [] + }, + { + "name": "getMessage", + "parameterTypes": [] + }, + { + "name": "getMissedIds", + "parameterTypes": [] + }, + { + "name": "getZipFilepath", + "parameterTypes": [] + }, + { + "name": "isSuccessful", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.downloading.IndexerSpecificDownloadExceptions", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.downloading.IndexerUniquenessScoreSaver", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "onNzbDownloadEvent", + "parameterTypes": [ + "org.nzbhydra.downloading.FileDownloadEvent" + ] + } + ] + }, +{ + "name":"org.nzbhydra.downloading.IndexerUniquenessScoreSaver$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, + { + "name": "org.nzbhydra.downloading.downloaders.AddNzbsResponse", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "getAddedIds", + "parameterTypes": [] + }, + { + "name": "getMessage", + "parameterTypes": [] + }, + { + "name": "getMissedIds", + "parameterTypes": [] + }, + { + "name": "isSuccessful", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.downloading.downloaders.DownloaderInstatiator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.downloading.downloaders.DownloaderProvider", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "handleNewConfig", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.downloaders.DownloaderStatus", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "getDownloadRateFormatted", + "parameterTypes": [] + }, + { + "name": "getDownloadRateInKilobytes", + "parameterTypes": [] + }, + { + "name": "getDownloaderName", + "parameterTypes": [] + }, + { + "name": "getDownloaderType", + "parameterTypes": [] + }, + { + "name": "getDownloadingRatesInKilobytes", + "parameterTypes": [] + }, + { + "name": "getDownloadingTitle", + "parameterTypes": [] + }, + { + "name": "getDownloadingTitlePercentFinished", + "parameterTypes": [] + }, + { + "name": "getDownloadingTitleRemainingSizeFormatted", + "parameterTypes": [] + }, + { + "name": "getDownloadingTitleRemainingTimeFormatted", + "parameterTypes": [] + }, + { + "name": "getElementsInQueue", + "parameterTypes": [] + }, + { + "name": "getLastDownloadRate", + "parameterTypes": [] + }, + { + "name": "getRemainingSeconds", + "parameterTypes": [] + }, + { + "name": "getRemainingSizeFormatted", + "parameterTypes": [] + }, + { + "name": "getRemainingSizeInMegaBytes", + "parameterTypes": [] + }, + { + "name": "getRemainingTimeFormatted", + "parameterTypes": [] + }, + { + "name": "getState", + "parameterTypes": [] + }, + { + "name": "getUrl", + "parameterTypes": [] + }, + { + "name": "isLastUpdateForNow", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.downloading.downloaders.DownloaderStatus$State", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.downloading.downloaders.DownloaderStatusRetrieval", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.downloading.downloaders.DownloaderWeb", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "addNzb", + "parameterTypes": [ + "org.nzbhydra.downloading.AddFilesRequest" + ] + }, + { + "name": "checkConnection", + "parameterTypes": [ + "org.nzbhydra.config.downloading.DownloaderConfig" + ] + }, + { + "name": "getStatus", + "parameterTypes": [] + } + ] + }, +{ + "name":"org.nzbhydra.downloading.downloaders.DownloaderWebSocket", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "handleNewConfig", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + }, + { + "name": "onShutdown", + "parameterTypes": [] + } + ] +}, + { + "name": "org.nzbhydra.downloading.downloaders.sabnzbd.mapping.AddNzbResponse", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setNzoIds", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setStatus", + "parameterTypes": [ + "boolean" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.downloaders.sabnzbd.mapping.Queue", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCache_art", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setCache_max", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setCache_size", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setCategories", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setDiskspace1", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDiskspace1_norm", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDiskspace2", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDiskspace2_norm", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDiskspacetotal1", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDiskspacetotal2", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setEta", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setFinish", + "parameterTypes": [ + "java.lang.Long" + ] + }, + { + "name": "setFinishaction", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setHave_quota", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "setHave_warnings", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setKbpersec", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setLeft_quota", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setLimit", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setLoadavg", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setMb", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setMbleft", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setNoofslots", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setNoofslots_total", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setPause_int", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPaused", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "setPaused_all", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "setQueue_details", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setQuota", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setRating_enable", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "setRefresh_rate", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setScripts", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setSize", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSizeleft", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSlots", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setSpeed", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSpeedlimit", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSpeedlimit_abs", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setStart", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setStatus", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setTimeleft", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setVersion", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.downloaders.sabnzbd.mapping.QueueEntry", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "org.nzbhydra.downloading.downloaders.sabnzbd.mapping.QueueResponse", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setQueue", + "parameterTypes": [ + "org.nzbhydra.downloading.downloaders.sabnzbd.mapping.Queue" + ] + } + ] + }, + { + "name": "org.nzbhydra.downloading.nzbs.NzbHandlingWeb", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "downloadNzbInternal", + "parameterTypes": [ + "long" + ] + }, + { + "name": "downloadNzbWithApikey", + "parameterTypes": [ + "long", + "java.lang.String" + ] + }, + { + "name": "downloadNzbZip", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getNzbZipData", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "saveNzbToBlackhole", + "parameterTypes": [ + "java.lang.Long" + ] + } + ] + }, +{ + "name":"org.nzbhydra.downloading.torrents.TorrentFileHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.downloading.torrents.TorrentFileHandler$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.downloading.torrents.TorrentHandlingWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.externaltools.ExternalTools", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.externaltools.ExternalToolsWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.fortests.DebugWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.genericstorage.GenericStorage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.genericstorage.GenericStorageWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.historystats.History", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.historystats.History$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.historystats.HistoryWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.historystats.HistoryWeb$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.historystats.Stats", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.historystats.Stats$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.historystats.StatsWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.Anizb$NewznabHandlingStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] +}, + { + "name": "org.nzbhydra.indexers.Binsearch$NewznabHandlingStrategy", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] + }, +{ + "name":"org.nzbhydra.indexers.DevIndexer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.nzbhydra.config.ConfigProvider", + "org.nzbhydra.indexers.IndexerRepository", + "org.nzbhydra.searching.db.SearchResultRepository", + "org.nzbhydra.indexers.IndexerApiAccessRepository", + "org.nzbhydra.indexers.IndexerApiAccessEntityShortRepository", + "org.nzbhydra.indexers.status.IndexerLimitRepository", + "org.nzbhydra.indexers.IndexerWebAccess", + "org.nzbhydra.searching.SearchResultAcceptor", + "org.nzbhydra.searching.CategoryProvider", + "org.nzbhydra.mediainfo.InfoProvider", + "org.springframework.context.ApplicationEventPublisher", + "org.nzbhydra.indexers.QueryGenerator", + "org.nzbhydra.searching.CustomQueryAndTitleMappingHandler", + "org.springframework.oxm.Unmarshaller", + "org.nzbhydra.config.BaseConfigHandler" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.DevIndexer$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.indexers.DevIndexer$DevIndexerHandlingStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.DogNzb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.nzbhydra.config.ConfigProvider", + "org.nzbhydra.indexers.IndexerRepository", + "org.nzbhydra.searching.db.SearchResultRepository", + "org.nzbhydra.indexers.IndexerApiAccessRepository", + "org.nzbhydra.indexers.IndexerApiAccessEntityShortRepository", + "org.nzbhydra.indexers.status.IndexerLimitRepository", + "org.nzbhydra.indexers.IndexerWebAccess", + "org.nzbhydra.searching.SearchResultAcceptor", + "org.nzbhydra.searching.CategoryProvider", + "org.nzbhydra.mediainfo.InfoProvider", + "org.springframework.context.ApplicationEventPublisher", + "org.nzbhydra.indexers.QueryGenerator", + "org.nzbhydra.searching.CustomQueryAndTitleMappingHandler", + "org.springframework.oxm.Unmarshaller", + "org.nzbhydra.config.BaseConfigHandler" + ] + } + ] +}, + { + "name": "org.nzbhydra.indexers.DogNzb$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods": [ + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] + }, +{ + "name":"org.nzbhydra.indexers.DogNzb$NewznabHandlingStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.Indexer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "cleanUpTitle", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "completeIndexerSearchResult", + "parameterTypes": [ + "java.lang.Object", + "org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult", + "org.nzbhydra.searching.SearchResultAcceptor$AcceptorResult", + "org.nzbhydra.searching.searchrequests.SearchRequest", + "int", + "java.lang.Integer" + ] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getConfig", + "parameterTypes": [] + }, + { + "name": "getIndexerEntity", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getSearchResultItems", + "parameterTypes": [ + "java.lang.Object", + "org.nzbhydra.searching.searchrequests.SearchRequest" + ] + }, + { + "name": "handleNewConfig", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "initialize", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig", + "org.nzbhydra.indexers.IndexerEntity" + ] + }, + { + "name": "search", + "parameterTypes": [ + "org.nzbhydra.searching.searchrequests.SearchRequest", + "int", + "java.lang.Integer" + ] + }, + { + "name": "toString", + "parameterTypes": [] + }, + { + "name": "tryParseDate", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.IndexerAccessResult" +}, +{ + "name":"org.nzbhydra.indexers.IndexerApiAccessEntity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.IndexerApiAccessEntityShort", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.IndexerApiAccessEntityShortRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.indexers.IndexerApiAccessEntityShort_" +}, +{ + "name":"org.nzbhydra.indexers.IndexerApiAccessEntity_" +}, +{ + "name":"org.nzbhydra.indexers.IndexerApiAccessRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.indexers.IndexerApiAccessType" +}, +{ + "name":"org.nzbhydra.indexers.IndexerEntity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.IndexerEntity_" +}, +{ + "name":"org.nzbhydra.indexers.IndexerHandlingStrategy", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.indexers.IndexerRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"save","parameterTypes":["java.lang.Object"] }] +}, +{ + "name":"org.nzbhydra.indexers.IndexerSearchEntity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getIndexerEntity", + "parameterTypes": [] + }, + { + "name": "getResultsCount", + "parameterTypes": [] + }, + { + "name": "getSearchEntity", + "parameterTypes": [] + }, + { + "name": "getSuccessful", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setIndexerEntity", + "parameterTypes": [ + "org.nzbhydra.indexers.IndexerEntity" + ] + }, + { + "name": "setResultsCount", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setSearchEntity", + "parameterTypes": [ + "org.nzbhydra.searching.db.SearchEntity" + ] + }, + { + "name": "setSuccessful", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, + { + "name": "org.nzbhydra.indexers.IndexerSearchEntity$HibernateProxy$Gx9qhr5z", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, +{ + "name":"org.nzbhydra.indexers.IndexerSearchEntity_" +}, +{ + "name":"org.nzbhydra.indexers.IndexerSearchRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.indexers.IndexerStatusesCleanupTask", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.nzbhydra.config.ConfigProvider", + "org.nzbhydra.config.BaseConfigHandler" + ] + }, + { + "name": "cleanup", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.IndexerWebAccess", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.Newznab", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "completeIndexerSearchResult", + "parameterTypes": [ + "java.lang.Object", + "org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult", + "org.nzbhydra.searching.SearchResultAcceptor$AcceptorResult", + "org.nzbhydra.searching.searchrequests.SearchRequest", + "int", + "java.lang.Integer" + ] + }, + { + "name": "getIdToCategory", + "parameterTypes": [] + }, + { + "name": "getIndexerStatusRepository", + "parameterTypes": [] + }, + { + "name": "getNfo", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getUnmarshaller", + "parameterTypes": [] + }, + { + "name": "setIndexerStatusRepository", + "parameterTypes": [ + "org.nzbhydra.indexers.status.IndexerLimitRepository" + ] + }, + { + "name": "setUnmarshaller", + "parameterTypes": [ + "org.springframework.oxm.Unmarshaller" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.Newznab$NewznabHandlingStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.NzbGeek$NewznabHandlingStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] +}, + { + "name": "org.nzbhydra.indexers.NzbIndex$NewznabHandlingStrategy", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] + }, +{ + "name":"org.nzbhydra.indexers.QueryGenerator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.capscheck.IndexerChecker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.capscheck.IndexerWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.capscheck.JacketConfigRetriever", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.capscheck.SimpleConnectionChecker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.status.IndexerLimit", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.status.IndexerLimitRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.indexers.status.IndexerLimit_" +}, +{ + "name":"org.nzbhydra.indexers.status.IndexerStatusesAndLimits", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.indexers.status.IndexerStatusesWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.indexers.torznab.Torznab$NewznabHandlingStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "handlesIndexerConfig", + "parameterTypes": [ + "org.nzbhydra.config.indexer.IndexerConfig" + ] + } + ] +}, +{ + "name":"org.nzbhydra.logging.ColorConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.logging.EceptionFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "decide", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "updateMarkersFilter", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.logging.LogAnonymizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.logging.LogContentProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.logging.LoggingMarkerFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "decide", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "handleConfigChangedEvent", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + }, + { + "name": "updateMarkersFilter", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.logging.MdcLogConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.logging.SensitiveDataRemovingPatternLayoutEncoder", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCharset", + "parameterTypes": [ + "java.nio.charset.Charset" + ] + } + ] +}, +{ + "name":"org.nzbhydra.mapping.github.Asset", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setBrowserDownloadUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setContentType", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setCreatedAt", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDownloadCount", + "parameterTypes": [ + "java.lang.Long" + ] + }, + { + "name": "setId", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setLabel", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSize", + "parameterTypes": [ + "java.lang.Long" + ] + }, + { + "name": "setState", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUpdatedAt", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUrl", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.mapping.github.Release", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setAssets", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setAssetsUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setBody", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setCreatedAt", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDraft", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "setHtmlUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setId", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPrerelease", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "setPublishedAt", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setTagName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setTarballUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setTargetCommitish", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUploadUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setZipballUrl", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, + { + "name": "org.nzbhydra.mapping.newznab.ActionAttribute", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "org.nzbhydra.mapping.newznab.NewznabParameters", + "allDeclaredFields": true, + "queryAllPublicMethods": true, + "queryAllPublicConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getApikey", + "parameterTypes": [] + }, + { + "name": "getQ", + "parameterTypes": [] + }, + { + "name": "getT", + "parameterTypes": [] + }, + { + "name": "setApikey", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setQ", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setT", + "parameterTypes": [ + "org.nzbhydra.mapping.newznab.ActionAttribute" + ] + } + ] + }, + { + "name": "org.nzbhydra.mapping.newznab.NewznabResponse", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getSearchType", + "parameterTypes": [] + }, + { + "name": "setSearchType", + "parameterTypes": [ + "org.nzbhydra.mapping.newznab.NewznabResponse$SearchType" + ] + } + ] + }, + { + "name": "org.nzbhydra.mapping.newznab.NewznabResponse$SearchType", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "fields": [ + { + "name": "JSON" + }, + { + "name": "NEWZNAB" + }, + { + "name": "TORZNAB" + } + ] + }, + { + "name": "org.nzbhydra.mapping.newznab.json.JsonPubdateDeserializer", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "deserialize", + "parameterTypes": [ + "com.fasterxml.jackson.core.JsonParser", + "com.fasterxml.jackson.databind.DeserializationContext" + ] + } + ] + }, +{ + "name":"org.nzbhydra.mapping.newznab.json.JsonPubdateSerializer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "serialize", + "parameterTypes": [ + "java.lang.Object", + "com.fasterxml.jackson.core.JsonGenerator", + "com.fasterxml.jackson.databind.SerializerProvider" + ] + } + ] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.JaxbPubdateAdapter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabAttribute", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabXmlApilimits", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabXmlChannel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabXmlEnclosure", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, + { + "name": "org.nzbhydra.mapping.newznab.xml.NewznabXmlError", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabXmlGuid", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabXmlItem", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.Xml", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlCategories", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlCategory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlLimits", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlRetention", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlRoot", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlSearch", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlSearching", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.CapsXmlServer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.jackett.JacketCapsXmlIndexer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.caps.jackett.JacketCapsXmlRoot", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mapping.newznab.xml.package-info" +}, +{ + "name":"org.nzbhydra.mediainfo.CustomTmdb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "initWithApiKey", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.mediainfo.InfoProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mediainfo.InfoProvider$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.mediainfo.MediaInfoWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mediainfo.MovieInfo", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.mediainfo.MovieInfoRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.mediainfo.MovieInfo_" +}, +{ + "name":"org.nzbhydra.mediainfo.TmdbHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.mediainfo.TvInfo", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.mediainfo.TvInfoRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.mediainfo.TvInfo_" +}, +{ + "name":"org.nzbhydra.mediainfo.TvMazeHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.misc.BrowserOpener", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.misc.OpenPortChecker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.misc.UserAgentMapper", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.misc.WebHooks", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "onNzbDownloadEvent", + "parameterTypes": [ + "org.nzbhydra.downloading.FileDownloadEvent" + ] + }, + { + "name": "onSearchEvent", + "parameterTypes": [ + "org.nzbhydra.searching.Searcher$SearchEvent" + ] + } + ] +}, +{ + "name":"org.nzbhydra.misc.WebHooks$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.news.NewsProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.news.NewsWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.news.ShownNews", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.news.ShownNewsRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.news.ShownNews_" +}, +{ + "name":"org.nzbhydra.notifications.NotificationEntity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.notifications.NotificationEntity$MessageType" +}, +{ + "name":"org.nzbhydra.notifications.NotificationEntity_" +}, + { + "name": "org.nzbhydra.notifications.NotificationHandler", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "handleNotification", + "parameterTypes": [ + "org.nzbhydra.notifications.NotificationEvent" + ] + } + ] + }, +{ + "name":"org.nzbhydra.notifications.NotificationRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.notifications.NotificationsWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "onShutdown", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.problemdetection.DeleteOldDatabaseBackupDetector", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "executeCheck", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.problemdetection.OpenPortProblemDetector", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "executeCheck", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.problemdetection.OutOfMemoryDetector", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "executeCheck", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.problemdetection.OutdatedWrapperDetector", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "executeCheck", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.problemdetection.ProblemDetector", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.problemdetection.ProblemDetectorTask", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "detectProblems", + "parameterTypes": [] + }, + { + "name": "init", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.problemdetection.VipExpiryDetector", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "executeCheck", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.searching.CategoryProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "handleNewConfigEvent", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + } + ] +}, + { + "name": "org.nzbhydra.searching.CustomQueryAndTitleMappingHandler", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, +{ + "name":"org.nzbhydra.searching.DuplicateDetector", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.searching.IndexerForSearchSelector", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "pickIndexers", + "parameterTypes": [ + "org.nzbhydra.searching.searchrequests.SearchRequest" + ] + } + ] +}, + { + "name": "org.nzbhydra.searching.IndexerForSearchSelector$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods": [ + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] + }, + { + "name": "org.nzbhydra.searching.IndexerInstantiator", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.searching.InternalSearchResultProcessor", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.searching.SearchModuleConfigProvider", + "allDeclaredFields": true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "handleNewConfig", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + } + ] +}, +{ + "name":"org.nzbhydra.searching.SearchModuleProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getIndexerByName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getIndexers", + "parameterTypes": [] + }, + { + "name": "loadIndexers", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.nzbhydra.searching.SearchModuleProvider$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.searching.SearchResultAcceptor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.searching.SearchWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "handleIndexerSearchFinishedEvent", + "parameterTypes": [ + "org.nzbhydra.searching.dtoseventsenums.IndexerSearchFinishedEvent" + ] + }, + { + "name": "handleIndexerSelectionEvent", + "parameterTypes": [ + "org.nzbhydra.searching.dtoseventsenums.IndexerSelectionEvent" + ] + }, + { + "name": "handleSearchMessageEvent", + "parameterTypes": [ + "org.nzbhydra.searching.dtoseventsenums.SearchMessageEvent" + ] + } + ] +}, +{ + "name":"org.nzbhydra.searching.Searcher", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "onShutdown", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.searching.cleanup.HistoryCleanupTask", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "deleteOldResults", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.searching.cleanup.OldResultsCleanupTask", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "deleteOldResults", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.searching.cleanup.OldResultsCleanupTask$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.searching.cleanup.ShortIndexerApiAccessCleanup", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "deleteOldResults", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.searching.cleanup.ShortIndexerApiAccessCleanup$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.searching.db.IdentifierKeyValuePair", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "java.lang.Integer" + ] + } + ] +}, +{ + "name":"org.nzbhydra.searching.db.IdentifierKeyValuePair_" +}, +{ + "name":"org.nzbhydra.searching.db.SearchEntity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.searching.db.SearchEntity_" +}, +{ + "name":"org.nzbhydra.searching.db.SearchRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.searching.db.SearchResultEntity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getDetails", + "parameterTypes": [] + }, + { + "name": "getDownloadType", + "parameterTypes": [] + }, + { + "name": "getFirstFound", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getIndexer", + "parameterTypes": [] + }, + { + "name": "getIndexerGuid", + "parameterTypes": [] + }, + { + "name": "getIndexerSearchEntity", + "parameterTypes": [] + }, + { + "name": "getLink", + "parameterTypes": [] + }, + { + "name": "getPubDate", + "parameterTypes": [] + }, + { + "name": "getTitle", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "setDetails", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDownloadType", + "parameterTypes": [ + "org.nzbhydra.searching.dtoseventsenums.SearchResultItem$DownloadType" + ] + }, + { + "name": "setFirstFound", + "parameterTypes": [ + "java.time.Instant" + ] + }, + { + "name": "setId", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setIndexer", + "parameterTypes": [ + "org.nzbhydra.indexers.IndexerEntity" + ] + }, + { + "name": "setIndexerGuid", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setIndexerSearchEntity", + "parameterTypes": [ + "org.nzbhydra.indexers.IndexerSearchEntity" + ] + }, + { + "name": "setLink", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPubDate", + "parameterTypes": [ + "java.time.Instant" + ] + }, + { + "name": "setTitle", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, + { + "name": "org.nzbhydra.searching.db.SearchResultEntity$HibernateProxy$pWMjAShB", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.searching.db.SearchResultEntity_" + }, + { + "name": "org.nzbhydra.searching.db.SearchResultRepository", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true + }, + { + "name":"org.nzbhydra.searching.db.SearchResultSequenceGenerator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.searching.dtoseventsenums.SearchResultItem", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.nzbhydra.searching.dtoseventsenums.SearchResultItem$DownloadType" +}, + { + "name": "org.nzbhydra.searching.searchrequests.SearchRequestFactory", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.searching.searchrequests.SearchSource" + }, + { + "name": "org.nzbhydra.searching.uniqueness.IndexerUniquenessScoreEntity", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "setId", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.nzbhydra.searching.uniqueness.IndexerUniquenessScoreEntityRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.nzbhydra.searching.uniqueness.IndexerUniquenessScoreEntity_" +}, +{ + "name":"org.nzbhydra.springconfig.AppConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getRestTemplate", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.springconfig.AppConfig$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "getRestTemplate", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.springconfig.AppConfig$$SpringCGLIB$$1", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.springconfig.AppConfig$$SpringCGLIB$$2", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.springconfig.ControllerAdvices", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "initBinder", + "parameterTypes": [ + "org.springframework.web.bind.WebDataBinder" + ] + } + ] +}, +{ + "name":"org.nzbhydra.springconfig.GracefulSpringShutdown", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "customize", + "parameterTypes": [ + "org.springframework.boot.web.server.WebServerFactory" + ] + } + ] +}, +{ + "name":"org.nzbhydra.systemcontrol.SystemControl", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.systemcontrol.SystemControlWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.tasks.HydraTask", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.nzbhydra.tasks.HydraTaskConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "taskExecutor", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.tasks.HydraTaskConfiguration$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "taskExecutor", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.tasks.HydraTaskConfiguration$$SpringCGLIB$$1", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.tasks.HydraTaskConfiguration$$SpringCGLIB$$2", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.tasks.HydraTaskScheduler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "afterSingletonsInstantiated", + "parameterTypes": [] + }, + { + "name": "onShutdown", + "parameterTypes": [] + }, + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.nzbhydra.tasks.HydraTasksWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.update.AutomaticUpdater", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "checkAndInstall", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.update.AutomaticUpdater$$SpringCGLIB$$0", + "fields": [ + { + "name": "CGLIB$CALLBACK_FILTER" + }, + { + "name": "CGLIB$FACTORY_DATA" + } + ], + "methods":[{"name":"CGLIB$SET_THREAD_CALLBACKS","parameterTypes":["org.springframework.cglib.proxy.Callback[]"] }] +}, +{ + "name":"org.nzbhydra.update.UpdateManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.update.UpdatesWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, + { + "name": "org.nzbhydra.web.ErrorHandler", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.web.HelpWeb", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "name": "org.nzbhydra.web.HydraErrorController", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.web.servlet.error.ErrorAttributes" + ] + } + ] + }, +{ + "name":"org.nzbhydra.web.Interceptor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "preHandle", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.nzbhydra.web.MainWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.web.NzbDetailsWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.web.UrlCalculator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.nzbhydra.web.WebConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "addFormatters", + "parameterTypes": [ + "org.springframework.format.FormatterRegistry" + ] + }, + { + "name": "addResourceHandlers", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry" + ] + }, + { + "name": "filterRegistrationBean", + "parameterTypes": [] + }, + { + "name": "marshaller", + "parameterTypes": [] + }, + { + "name": "requestMappingHandlerMapping", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager", + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + } + ] +}, +{ + "name":"org.nzbhydra.web.WebConfiguration$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "beanNameHandlerMapping", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "defaultServletHandlerMapping", + "parameterTypes": [] + }, + { + "name": "filterRegistrationBean", + "parameterTypes": [] + }, + { + "name": "flashMapManager", + "parameterTypes": [] + }, + { + "name": "handlerExceptionResolver", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager" + ] + }, + { + "name": "handlerFunctionAdapter", + "parameterTypes": [] + }, + { + "name": "httpRequestHandlerAdapter", + "parameterTypes": [] + }, + { + "name": "localeResolver", + "parameterTypes": [] + }, + { + "name": "marshaller", + "parameterTypes": [] + }, + { + "name": "mvcContentNegotiationManager", + "parameterTypes": [] + }, + { + "name": "mvcConversionService", + "parameterTypes": [] + }, + { + "name": "mvcHandlerMappingIntrospector", + "parameterTypes": [] + }, + { + "name": "mvcPathMatcher", + "parameterTypes": [] + }, + { + "name": "mvcPatternParser", + "parameterTypes": [] + }, + { + "name": "mvcResourceUrlProvider", + "parameterTypes": [] + }, + { + "name": "mvcUriComponentsContributor", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" + ] + }, + { + "name": "mvcUrlPathHelper", + "parameterTypes": [] + }, + { + "name": "mvcValidator", + "parameterTypes": [] + }, + { + "name": "mvcViewResolver", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager" + ] + }, + { + "name": "requestMappingHandlerAdapter", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager", + "org.springframework.format.support.FormattingConversionService", + "org.springframework.validation.Validator" + ] + }, + { + "name": "requestMappingHandlerMapping", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager", + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "resourceHandlerMapping", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager", + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "routerFunctionMapping", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "simpleControllerHandlerAdapter", + "parameterTypes": [] + }, + { + "name": "themeResolver", + "parameterTypes": [] + }, + { + "name": "viewControllerHandlerMapping", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "viewNameTranslator", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.web.WebConfiguration$$SpringCGLIB$$1", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.web.WebConfiguration$$SpringCGLIB$$2", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.nzbhydra.web.WebSocketConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "configureClientInboundChannel", + "parameterTypes": [ + "org.springframework.messaging.simp.config.ChannelRegistration" + ] + }, + { + "name": "configureClientOutboundChannel", + "parameterTypes": [ + "org.springframework.messaging.simp.config.ChannelRegistration" + ] + }, + { + "name": "configureMessageBroker", + "parameterTypes": [ + "org.springframework.messaging.simp.config.MessageBrokerRegistry" + ] + }, + { + "name": "onShutdown", + "parameterTypes": [] + }, + { + "name": "registerStompEndpoints", + "parameterTypes": [ + "org.springframework.web.socket.config.annotation.StompEndpointRegistry" + ] + } + ] +}, +{ + "name":"org.nzbhydra.web.WebSocketConfig$$SpringCGLIB$$0", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"CGLIB$FACTORY_DATA"}], + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "CGLIB$SET_STATIC_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + }, + { + "name": "CGLIB$SET_THREAD_CALLBACKS", + "parameterTypes": [ + "org.springframework.cglib.proxy.Callback[]" + ] + } + ] +}, +{ + "name":"org.nzbhydra.web.WelcomeWeb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "createRequest", + "parameterTypes": [ + "java.net.URI", + "org.springframework.http.HttpMethod" + ] + }, + { + "name": "handleConfigChangedEvent", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + }, + { + "name": "init", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.webaccess.Ssl", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "handleConfigChangedEvent", + "parameterTypes": [ + "org.nzbhydra.config.ConfigChangedEvent" + ] + }, + { + "name": "init", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.nzbhydra.webaccess.WebAccess", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.quartz.Scheduler" +}, +{ + "name":"org.reactivestreams$Publisher" +}, +{ + "name":"org.reactivestreams.Publisher" +}, +{ + "name":"org.slf4j.Logger" +}, +{ + "name":"org.slf4j.LoggerFactory" +}, +{ + "name":"org.slf4j.bridge.SLF4JBridgeHandler" +}, +{ + "name":"org.slf4j.spi.LocationAwareLogger", + "queryAllDeclaredMethods":true, + "methods":[{"name":"log","parameterTypes":["org.slf4j.Marker","java.lang.String","int","java.lang.String","java.lang.Object[]","java.lang.Throwable"] }] +}, +{ + "name":"org.slf4j.spi.SLF4JServiceProvider" +}, +{ + "name":"org.springframework.amqp.rabbit.core.RabbitTemplate" +}, +{ + "name":"org.springframework.aop.Advisor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.aop.PointcutAdvisor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.aop.SpringProxy", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.aop.TargetClassAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getTargetClass","parameterTypes":[] }] +}, +{ + "name":"org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "determineBeanType", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "setBeforeExistingAdvisors", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.aop.framework.Advised", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addAdvisor", + "parameterTypes": [ + "org.springframework.aop.Advisor" + ] + }, + { + "name": "getTargetSource", + "parameterTypes": [] + }, + { + "name": "isFrozen", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.aop.framework.AopInfrastructureBean", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.aop.framework.ProxyConfig", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "copyFrom", + "parameterTypes": [ + "org.springframework.aop.framework.ProxyConfig" + ] + }, + { + "name": "isExposeProxy", + "parameterTypes": [] + }, + { + "name": "isFrozen", + "parameterTypes": [] + }, + { + "name": "isOpaque", + "parameterTypes": [] + }, + { + "name": "isOptimize", + "parameterTypes": [] + }, + { + "name": "isProxyTargetClass", + "parameterTypes": [] + }, + { + "name": "setExposeProxy", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setFrozen", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setOpaque", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setOptimize", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setProxyTargetClass", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.aop.framework.ProxyProcessorSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setOrder", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setProxyClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + } + ] +}, +{ + "name":"org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setBeanFactory","parameterTypes":["org.springframework.beans.factory.BeanFactory"] }] +}, +{ + "name":"org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "determineBeanType", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "determineCandidateConstructors", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "getEarlyBeanReference", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "isFrozen", + "parameterTypes": [] + }, + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInstantiation", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "postProcessProperties", + "parameterTypes": [ + "org.springframework.beans.PropertyValues", + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "predictBeanType", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "setAdvisorAdapterRegistry", + "parameterTypes": [ + "org.springframework.aop.framework.adapter.AdvisorAdapterRegistry" + ] + }, + { + "name": "setApplyCommonInterceptorsFirst", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setCustomTargetSourceCreators", + "parameterTypes": [ + "org.springframework.aop.framework.autoproxy.TargetSourceCreator[]" + ] + }, + { + "name": "setFrozen", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setInterceptorNames", + "parameterTypes": [ + "java.lang.String[]" + ] + } + ] +}, +{ + "name":"org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setBeanFactory","parameterTypes":["org.springframework.beans.factory.BeanFactory"] }] +}, +{ + "name":"org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.aop.scope.ScopedObject", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.aop.scope.ScopedProxyFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "isSingleton", + "parameterTypes": [] + }, + { + "name": "setTargetBeanName", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getAdvice", + "parameterTypes": [] + }, + { + "name": "getAdviceBeanName", + "parameterTypes": [] + }, + { + "name": "setAdvice", + "parameterTypes": [ + "org.aopalliance.aop.Advice" + ] + }, + { + "name": "setAdviceBeanName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.aop.support.AbstractPointcutAdvisor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isPerInstance", + "parameterTypes": [] + }, + { + "name": "setOrder", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.springframework.aot.hint.annotation.Reflective", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.aot.hint.annotation.RegisterReflectionForBinding", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.batch.core.launch.JobLauncher" +}, +{ + "name":"org.springframework.beans.factory.Aware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.BeanClassLoaderAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.BeanFactoryAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.BeanNameAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.DisposableBean", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.FactoryBean", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"isSingleton","parameterTypes":[] }] +}, +{ + "name":"org.springframework.beans.factory.InitializingBean", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.SmartInitializingSingleton", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.annotation.Autowired", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.beans.factory.annotation.Qualifier", + "queryAllDeclaredMethods":true, + "methods":[{"name":"value","parameterTypes":[] }] +}, +{ + "name":"org.springframework.beans.factory.annotation.Value", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.aot.BeanRegistrationAotProcessor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"isBeanExcludedFromAotProcessing","parameterTypes":[] }] +}, +{ + "name":"org.springframework.beans.factory.config.AbstractFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "getObject", + "parameterTypes": [] + }, + { + "name": "isSingleton", + "parameterTypes": [] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "setSingleton", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.beans.factory.config.BeanFactoryPostProcessor", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.config.BeanPostProcessor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "postProcessAfterInstantiation", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInstantiation", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "postProcessProperties", + "parameterTypes": [ + "org.springframework.beans.PropertyValues", + "java.lang.Object", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor", + "queryAllPublicMethods":true, + "methods": [ + { + "name": "determineCandidateConstructors", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + }, + { + "name": "getEarlyBeanReference", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "predictBeanType", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor", + "queryAllPublicMethods":true, + "methods":[{"name":"resetBeanDefinition","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"org.springframework.beans.factory.support.NullBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.beans.factory.xml.XmlBeanDefinitionReader", + "allPublicFields":true +}, +{ + "name":"org.springframework.boot.ClearCachesApplicationListener", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.LazyInitializationExcludeFilter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.SpringApplication", + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.springframework.boot.actuate.audit.AuditEventRepository" +}, +{ + "name":"org.springframework.boot.actuate.audit.AuditEventsEndpoint" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.availability.AvailabilityProbesAutoConfiguration$ProbesCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "beansEndpoint", + "parameterTypes": [ + "org.springframework.context.ConfigurableApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "cachesEndpoint", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "cachesEndpointWebExtension", + "parameterTypes": [ + "org.springframework.boot.actuate.cache.CachesEndpoint" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "conditionsReportEndpoint", + "parameterTypes": [ + "org.springframework.context.ConfigurableApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "configurationPropertiesReportEndpoint", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "configurationPropertiesReportEndpointWebExtension", + "parameterTypes": [ + "org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint", + "org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "endpointCachingOperationInvokerAdvisor", + "parameterTypes": [ + "org.springframework.core.env.Environment" + ] + }, + { + "name": "endpointOperationParameterMapper", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.condition.OnAvailableEndpointCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"match","parameterTypes":["org.springframework.boot.actuate.endpoint.ExposableEndpoint"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "endpointObjectMapper", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.MappingWebEndpointPathMapper", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getRootPath","parameterTypes":["org.springframework.boot.actuate.endpoint.EndpointId"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "servletExposeExcludePropertyEndpointFilter", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "servletEndpointRegistrar", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties", + "org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier", + "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties" + ] + }, + { + "name": "controllerEndpointDiscoverer", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "controllerExposeExcludePropertyEndpointFilter", + "parameterTypes": [] + }, + { + "name": "endpointMediaTypes", + "parameterTypes": [] + }, + { + "name": "pathMappedEndpoints", + "parameterTypes": [ + "java.util.Collection" + ] + }, + { + "name": "webEndpointDiscoverer", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper", + "org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "webEndpointPathMapper", + "parameterTypes": [] + }, + { + "name": "webExposeExcludePropertyEndpointFilter", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration$WebEndpointServletConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "servletEndpointDiscoverer", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getExposure", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties$Exposure", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "getInclude", + "parameterTypes": [] + }, + { + "name": "setInclude", + "parameterTypes": [ + "java.util.Set" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "controllerEndpointHandlerMapping", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier", + "org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties", + "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties" + ] + }, + { + "name": "endpointObjectMapperWebMvcConfigurer", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper" + ] + }, + { + "name": "webEndpointServletHandlerMapping", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier", + "org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier", + "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier", + "org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes", + "org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties", + "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties", + "org.springframework.core.env.Environment" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration$EndpointObjectMapperWebMvcConfigurer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"configureMessageConverters","parameterTypes":["java.util.List"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "environmentEndpoint", + "parameterTypes": [ + "org.springframework.core.env.Environment", + "org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "environmentEndpointWebExtension", + "parameterTypes": [ + "org.springframework.boot.actuate.env.EnvironmentEndpoint", + "org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.flyway.FlywayEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "flywayEndpoint", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.AutoConfiguredHealthContributorRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"registerContributor","parameterTypes":["java.lang.String","java.lang.Object"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.AutoConfiguredHealthEndpointGroups", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "get", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getNames", + "parameterTypes": [] + }, + { + "name": "getPrimary", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "pingHealthContributor", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "healthContributorRegistry", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.boot.actuate.health.HealthEndpointGroups", + "java.util.Map", + "java.util.Map" + ] + }, + { + "name": "healthEndpoint", + "parameterTypes": [ + "org.springframework.boot.actuate.health.HealthContributorRegistry", + "org.springframework.boot.actuate.health.HealthEndpointGroups", + "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties" + ] + }, + { + "name": "healthEndpointGroups", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties" + ] + }, + { + "name": "healthEndpointGroupsBeanPostProcessor", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "healthHttpCodeStatusMapper", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties" + ] + }, + { + "name": "healthStatusAggregator", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration$HealthEndpointGroupsBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"postProcessAfterInitialization","parameterTypes":["java.lang.Object","java.lang.String"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getShowDetails", + "parameterTypes": [] + }, + { + "name": "setShowDetails", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.Show" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointReactiveWebExtensionConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "healthEndpointWebExtension", + "parameterTypes": [ + "org.springframework.boot.actuate.health.HealthContributorRegistry", + "org.springframework.boot.actuate.health.HealthEndpointGroups", + "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration$MvcAdditionalHealthEndpointPathsConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "healthEndpointWebMvcHandlerMapping", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier", + "org.springframework.boot.actuate.health.HealthEndpointGroups" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.HealthProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getRoles", + "parameterTypes": [] + }, + { + "name": "getShowComponents", + "parameterTypes": [] + }, + { + "name": "getStatus", + "parameterTypes": [] + }, + { + "name": "setRoles", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setShowComponents", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.Show" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.OnEnabledHealthIndicatorCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.health.ReactiveHealthEndpointConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.info.ConditionalOnEnabledInfoContributor", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.info.InfoContributorFallback" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.info.InfoContributorProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "infoEndpoint", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.info.OnEnabledInfoContributorCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "dbHealthContributor", + "parameterTypes": [ + "java.util.Map", + "org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration$LogFileCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "loggersEndpoint", + "parameterTypes": [ + "org.springframework.boot.logging.LoggingSystem", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration$OnEnabledLoggingSystemCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "heapDumpWebEndpoint", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "dumpEndpoint", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryConfiguration$MultipleNonPrimaryMeterRegistriesCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "classLoaderMetrics", + "parameterTypes": [] + }, + { + "name": "jvmCompilationMetrics", + "parameterTypes": [] + }, + { + "name": "jvmGcMetrics", + "parameterTypes": [] + }, + { + "name": "jvmHeapPressureMetrics", + "parameterTypes": [] + }, + { + "name": "jvmInfoMetrics", + "parameterTypes": [] + }, + { + "name": "jvmMemoryMetrics", + "parameterTypes": [] + }, + { + "name": "jvmThreadMetrics", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "logbackMetrics", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration$LogbackLoggingCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterSingletonsInstantiated", + "parameterTypes": [] + }, + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "meterRegistryPostProcessor", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "micrometerClock", + "parameterTypes": [] + }, + { + "name": "propertiesMeterFilter", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "metricsEndpoint", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.NoOpMeterRegistryConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.PropertiesMeterFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "accept", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter$Id" + ] + }, + { + "name": "configure", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter$Id", + "io.micrometer.core.instrument.distribution.DistributionStatisticConfig" + ] + }, + { + "name": "map", + "parameterTypes": [ + "io.micrometer.core.instrument.Meter$Id" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "diskSpaceMetrics", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties" + ] + }, + { + "name": "fileDescriptorMetrics", + "parameterTypes": [] + }, + { + "name": "processorMetrics", + "parameterTypes": [] + }, + { + "name": "uptimeMetrics", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMeterBinderProvidersConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMeterBinderProvidersConfiguration$CaffeineCacheMeterBinderProviderConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "caffeineCacheMeterBinderProvider", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsRegistrarConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry", + "java.util.Collection", + "java.util.Map" + ] + }, + { + "name": "cacheMetricsRegistrar", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.data.MetricsRepositoryMethodInvocationListenerBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"postProcessBeforeInitialization","parameterTypes":["java.lang.Object","java.lang.String"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.data.RepositoryMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties" + ] + }, + { + "name": "metricsRepositoryMethodInvocationListener", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.boot.actuate.metrics.data.RepositoryTagsProvider" + ] + }, + { + "name": "metricsRepositoryMethodInvocationListenerBeanPostProcessor", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "repositoryTagsProvider", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.export.OnMetricsExportEnabledCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "simpleConfig", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties" + ] + }, + { + "name": "simpleMeterRegistry", + "parameterTypes": [ + "io.micrometer.core.instrument.simple.SimpleConfig", + "io.micrometer.core.instrument.Clock" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimplePropertiesConfigAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "get", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "mode", + "parameterTypes": [] + }, + { + "name": "prefix", + "parameterTypes": [] + }, + { + "name": "step", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "dataSourcePoolMetadataMeterBinder", + "parameterTypes": [ + "java.util.Map", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration$DataSourcePoolMetadataMeterBinder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "hikariDataSourceMeterBinder", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration$HikariDataSourceMeterBinder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsListenerAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "startupTimeMetrics", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "bindTaskExecutorsToRegistry", + "parameterTypes": [ + "java.util.Map", + "io.micrometer.core.instrument.MeterRegistry" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "tomcatMetricsBinder", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "observationRegistry", + "parameterTypes": [] + }, + { + "name": "observationRegistryPostProcessor", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$MeterObservationHandlerConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$MeterObservationHandlerConfiguration$OnlyMetricsMeterObservationHandlerConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "defaultMeterObservationHandler", + "parameterTypes": [ + "io.micrometer.core.instrument.MeterRegistry" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$OnlyMetricsConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "metricsObservationHandlerGrouping", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.ObservationHandlerGrouping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"postProcessAfterInitialization","parameterTypes":["java.lang.Object","java.lang.String"] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration$MeterFilterConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "metricsHttpClientUriTagFilter", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties", + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.web.client.RestTemplateObservationConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "observationRestTemplateCustomizer", + "parameterTypes": [ + "io.micrometer.observation.ObservationRegistry", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties", + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.web.client.WebClientObservationConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties", + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties" + ] + }, + { + "name": "webMvcObservationFilter", + "parameterTypes": [ + "io.micrometer.observation.ObservationRegistry", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration$MeterFilterConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "metricsHttpServerUriTagFilter", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties", + "org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "scheduledTasksEndpoint", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.security.servlet.SecurityRequestMatchersManagementContextConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.security.servlet.SecurityRequestMatchersManagementContextConfiguration$MvcRequestMatcherConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "requestMatcherProvider", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfiguration$ApplicationStartupCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "diskSpaceHealthIndicator", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.ManagementContextFactory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.ManagementContextType" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "mappingsEndpoint", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "filterMappingDescriptionProvider", + "parameterTypes": [] + }, + { + "name": "servletMappingDescriptionProvider", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration$SpringMvcConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "dispatcherServletMappingDescriptionProvider", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.EnableManagementContext", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration$SameManagementContextConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.core.env.Environment" + ] + }, + { + "name": "afterSingletonsInstantiated", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration$SameManagementContextConfiguration$EnableSameManagementContextConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextConfigurationImportSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType" +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.server.OnManagementPortCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.servlet.ManagementServletContext", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "managementServletContext", + "parameterTypes": [ + "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties" + ] + }, + { + "name": "servletWebChildContextFactory", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.beans.BeansEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.cache.CachesEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.cache.CachesEndpointWebExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.context.ShutdownEndpoint" +}, +{ + "name":"org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"setApplicationContext","parameterTypes":["org.springframework.context.ApplicationContext"] }] +}, +{ + "name":"org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.EndpointFilter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.EndpointsSupplier", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.DeleteOperation", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.Endpoint", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.EndpointConverter", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "createOperationKey", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.Operation" + ] + }, + { + "name": "getEndpoints", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.EndpointExtension", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.ReadOperation", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.Selector", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.annotation.WriteOperation", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"mapParameterValue","parameterTypes":["org.springframework.boot.actuate.endpoint.invoke.OperationParameter","java.lang.Object"] }] +}, +{ + "name":"org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"apply","parameterTypes":["org.springframework.boot.actuate.endpoint.EndpointId","org.springframework.boot.actuate.endpoint.OperationType","org.springframework.boot.actuate.endpoint.invoke.OperationParameters","org.springframework.boot.actuate.endpoint.invoke.OperationInvoker"] }] +}, +{ + "name":"org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"iterator","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.PathMapper", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"onStartup","parameterTypes":["jakarta.servlet.ServletContext"] }] +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointDiscoverer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointFilter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getEndpoints", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$LinksHandler", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping$WebMvcLinksHandler", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.actuate.env.EnvironmentEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.env.EnvironmentEndpointWebExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.flyway.FlywayEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.AbstractHealthIndicator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"health","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.health.ContributorRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.DefaultContributorRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"registerContributor","parameterTypes":["java.lang.String","java.lang.Object"] }] +}, +{ + "name":"org.springframework.boot.actuate.health.DefaultHealthContributorRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getContributor", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "iterator", + "parameterTypes": [] + }, + { + "name": "registerContributor", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object" + ] + }, + { + "name": "unregisterContributor", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.health.HealthContributor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.HealthContributorRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.HealthEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.HealthEndpointGroups", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "get", + "parameterTypes": [ + "org.springframework.boot.actuate.health.AdditionalHealthEndpointPath" + ] + }, + { + "name": "getAllWithAdditionalPath", + "parameterTypes": [ + "org.springframework.boot.actuate.endpoint.web.WebServerNamespace" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.health.HealthEndpointSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getHealth","parameterTypes":["java.lang.Object","boolean"] }] +}, +{ + "name":"org.springframework.boot.actuate.health.HealthEndpointWebExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.HealthIndicator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getHealth","parameterTypes":["boolean"] }] +}, +{ + "name":"org.springframework.boot.actuate.health.HttpCodeStatusMapper", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.NamedContributors", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"stream","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.health.PingHealthIndicator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getStatusCode","parameterTypes":["org.springframework.boot.actuate.health.Status"] }] +}, +{ + "name":"org.springframework.boot.actuate.health.SimpleStatusAggregator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getAggregateStatus","parameterTypes":["java.util.Set"] }] +}, +{ + "name":"org.springframework.boot.actuate.health.StatusAggregator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getAggregateStatus","parameterTypes":["org.springframework.boot.actuate.health.Status[]"] }] +}, +{ + "name":"org.springframework.boot.actuate.info.InfoEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"afterPropertiesSet","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.actuate.logging.LogFileWebEndpoint" +}, +{ + "name":"org.springframework.boot.actuate.logging.LoggersEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.management.HeapDumpWebEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.management.ThreadDumpEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.metrics.MetricsEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.metrics.cache.CacheMeterBinderProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.metrics.cache.CacheMetricsRegistrar", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.metrics.cache.CaffeineCacheMeterBinderProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getMeterBinder","parameterTypes":["org.springframework.cache.Cache","java.lang.Iterable"] }] +}, +{ + "name":"org.springframework.boot.actuate.metrics.data.DefaultRepositoryTagsProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"repositoryTags","parameterTypes":["org.springframework.data.repository.core.support.RepositoryMethodInvocationListener$RepositoryMethodInvocation"] }] +}, +{ + "name":"org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"afterInvocation","parameterTypes":["org.springframework.data.repository.core.support.RepositoryMethodInvocationListener$RepositoryMethodInvocation"] }] +}, +{ + "name":"org.springframework.boot.actuate.metrics.data.RepositoryTagsProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.metrics.startup.StartupTimeMetricsListener", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "onApplicationEvent", + "parameterTypes": [ + "org.springframework.context.ApplicationEvent" + ] + }, + { + "name": "supportsEventType", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.metrics.system.DiskSpaceMetricsBinder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"bindTo","parameterTypes":["io.micrometer.core.instrument.MeterRegistry"] }] +}, +{ + "name":"org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"customize","parameterTypes":["org.springframework.web.client.RestTemplate"] }] +}, +{ + "name":"org.springframework.boot.actuate.metrics.web.tomcat.TomcatMetricsBinder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "onApplicationEvent", + "parameterTypes": [ + "org.springframework.context.ApplicationEvent" + ] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.system.DiskSpaceHealthIndicator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository" +}, +{ + "name":"org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint" +}, +{ + "name":"org.springframework.boot.actuate.web.mappings.MappingDescriptionProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods": true +}, +{ + "name":"org.springframework.boot.actuate.web.mappings.MappingsEndpoint", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.actuate.web.mappings.servlet.DispatcherServletsMappingDescriptionProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "describeMappings", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "getMappingName", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.web.mappings.servlet.FiltersMappingDescriptionProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "describeMappings", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "getMappingName", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.actuate.web.mappings.servlet.ServletsMappingDescriptionProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "describeMappings", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "getMappingName", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfiguration", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigurationImportSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigurationImportSelector$AutoConfigurationGroup", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigurationPackage", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigurationPackages$Registrar", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigureAfter", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigureBefore", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.AutoConfigureOrder", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.BackgroundPreinitializer", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.EnableAutoConfiguration", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.aop.AopAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "applicationAvailability", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.batch.JobRepositoryDependsOnDatabaseInitializationDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnBean", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnClass", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnJndi", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnProperty", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnResource", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication$Type" +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.OnBeanCondition", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.OnClassCondition", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.OnCloudPlatformCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.OnJndiCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.OnPropertyCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.OnResourceCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.condition.SearchStrategy" +}, +{ + "name":"org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "defaultLifecycleProcessor", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.context.LifecycleProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.context.LifecycleProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration$ResourceBundleCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "propertySourcesPlaceholderConfigurer", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "persistenceExceptionTranslationPostProcessor", + "parameterTypes": [ + "org.springframework.core.env.Environment" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration$BootstrapExecutorCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration$JpaRepositoriesImportSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesRegistrar", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties" + ] + }, + { + "name": "pageableCustomizer", + "parameterTypes": [] + }, + { + "name": "sortCustomizer", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "flywayDefaultDdlModeProvider", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "stringOrNumberMigrationVersionConverter", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration$FlywayAutoConfigurationRuntimeHints" +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration$FlywayConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "flyway", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.flyway.FlywayProperties", + "org.springframework.core.io.ResourceLoader", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.boot.autoconfigure.flyway.ResourceProviderCustomizer" + ] + }, + { + "name": "flywayInitializer", + "parameterTypes": [ + "org.flywaydb.core.Flyway", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "resourceProviderCustomizer", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration$FlywayDataSourceCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration$StringOrNumberToMigrationVersionConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "convert", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.core.convert.TypeDescriptor", + "org.springframework.core.convert.TypeDescriptor" + ] + }, + { + "name": "getConvertibleTypes", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayDataSource", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywayProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getLocations", + "parameterTypes": [] + }, + { + "name": "getSchemas", + "parameterTypes": [] + }, + { + "name": "setEnabled", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setLocations", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setSchemas", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.FlywaySchemaManagementProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getSchemaManagement","parameterTypes":["javax.sql.DataSource"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.flyway.ResourceProviderCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "gson", + "parameterTypes": [ + "com.google.gson.GsonBuilder" + ] + }, + { + "name": "gsonBuilder", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "standardGsonBuilderCustomizer", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.gson.GsonProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration$StandardGsonBuilderCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "customize", + "parameterTypes": [ + "com.google.gson.GsonBuilder" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.gson.GsonBuilderCustomizer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.gson.GsonProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.GsonHttpMessageConvertersConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.GsonHttpMessageConvertersConfiguration$JacksonAndJsonbUnavailableCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.GsonHttpMessageConvertersConfiguration$PreferGsonOrJacksonAndJsonbUnavailableCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.HttpMessageConverters", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"iterator","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "messageConverters", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$HttpMessageConvertersAutoConfigurationRuntimeHints" +}, +{ + "name":"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$NotReactiveWebApplicationCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "stringHttpMessageConverter", + "parameterTypes": [ + "org.springframework.core.env.Environment" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "mappingJackson2HttpMessageConverter", + "parameterTypes": [ + "com.fasterxml.jackson.databind.ObjectMapper" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.http.JsonbHttpMessageConvertersConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.boot.autoconfigure.info.ProjectInfoProperties"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration$GitResourceAvailableCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.info.ProjectInfoProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "jsonComponentModule", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "standardJacksonObjectMapperBuilderCustomizer", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.jackson.JacksonProperties", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "customize", + "parameterTypes": [ + "org.springframework.http.converter.json.Jackson2ObjectMapperBuilder" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonMixinConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "jsonMixinModule", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.boot.jackson.JsonMixinModuleEntries" + ] + }, + { + "name": "jsonMixinModuleEntries", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "jacksonObjectMapperBuilder", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "java.util.List" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "jacksonObjectMapper", + "parameterTypes": [ + "org.springframework.http.converter.json.Jackson2ObjectMapperBuilder" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$ParameterNamesModuleConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "parameterNamesModule", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jackson.JacksonProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getDeserialization", + "parameterTypes": [] + }, + { + "name": "getSerialization", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$EmbeddedDatabaseCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$PooledDataSourceAvailableCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$PooledDataSourceCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$PooledDataSourceConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Dbcp2" +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Generic" +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "dataSource", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.jdbc.DataSourceProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$OracleUcp" +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Tomcat" +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceJmxConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setDriverClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUsername", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration$JdbcTransactionManagerConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.JdbcProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.JdbcTemplateConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "jdbcTemplate", + "parameterTypes": [ + "javax.sql.DataSource", + "org.springframework.boot.autoconfigure.jdbc.JdbcProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.NamedParameterJdbcTemplateConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "namedParameterJdbcTemplate", + "parameterTypes": [ + "org.springframework.jdbc.core.JdbcTemplate" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "hikariPoolDataSourceMetadataProvider", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["javax.sql.DataSource","org.springframework.boot.autoconfigure.orm.jpa.JpaProperties","org.springframework.beans.factory.config.ConfigurableListableBeanFactory","org.springframework.beans.factory.ObjectProvider","org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties","org.springframework.beans.factory.ObjectProvider","org.springframework.beans.factory.ObjectProvider","org.springframework.beans.factory.ObjectProvider","org.springframework.beans.factory.ObjectProvider","org.springframework.beans.factory.ObjectProvider"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "entityManagerFactory", + "parameterTypes": [ + "org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder", + "org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes" + ] + }, + { + "name": "entityManagerFactoryBuilder", + "parameterTypes": [ + "org.springframework.orm.jpa.JpaVendorAdapter", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "jpaVendorAdapter", + "parameterTypes": [] + }, + { + "name": "transactionManager", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration$PersistenceManagedTypesConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "persistenceManagedTypes", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory", + "org.springframework.core.io.ResourceLoader" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.orm.jpa.JpaProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getProperties", + "parameterTypes": [] + }, + { + "name": "setDatabasePlatform", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setOpenInView", + "parameterTypes": [ + "java.lang.Boolean" + ] + }, + { + "name": "setProperties", + "parameterTypes": [ + "java.util.Map" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.quartz.SchedulerDependsOnDatabaseInitializationDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.security.DefaultWebSecurityCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.security.SecurityDataConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.security.SecurityProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration$ReactiveUserDetailsServiceCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getRequestMatcher","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "authenticationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "securityFilterChainRegistration", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.security.SecurityProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.session.JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "dataSourceScriptDatabaseInitializer", + "parameterTypes": [ + "javax.sql.DataSource", + "org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.sql.init.R2dbcInitializationConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.sql.init.SqlDataSourceScriptDatabaseInitializer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration$SqlInitializationModeCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.sql.init.SqlR2dbcScriptDatabaseInitializer" +}, +{ + "name":"org.springframework.boot.autoconfigure.task.ScheduledBeanLazyInitializationExcludeFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"isExcluded","parameterTypes":["java.lang.String","org.springframework.beans.factory.config.BeanDefinition","java.lang.Class"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "taskExecutorBuilder", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.task.TaskExecutionProperties", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.task.TaskExecutionProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "scheduledBeanLazyInitializationExcludeFilter", + "parameterTypes": [] + }, + { + "name": "taskSchedulerBuilder", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.task.TaskSchedulingProperties", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.task.TaskSchedulingProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.TemplateEngineConfigurations$DefaultTemplateEngineConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "templateEngine", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.TemplateEngineConfigurations$ReactiveTemplateEngineConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$DefaultTemplateResolverConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties", + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "defaultTemplateResolver", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafWebMvcConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafWebMvcConfiguration$ThymeleafViewResolverConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "thymeleafViewResolver", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties", + "org.thymeleaf.spring6.SpringTemplateEngine" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCache", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setMode", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.transaction.PlatformTransactionManagerCustomizer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "platformTransactionManagerCustomizers", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$TransactionTemplateConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "transactionTemplate", + "parameterTypes": [ + "org.springframework.transaction.PlatformTransactionManager" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.transaction.TransactionProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "customize", + "parameterTypes": [ + "org.springframework.transaction.PlatformTransactionManager" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.transaction.jta.JndiJtaConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.validation.PrimaryDefaultValidatorPostProcessor", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "defaultValidator", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "methodValidationPostProcessor", + "parameterTypes": [ + "org.springframework.core.env.Environment", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.web.OnEnabledResourceChainCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.ServerProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getCompression", + "parameterTypes": [] + }, + { + "name": "getServlet", + "parameterTypes": [] + }, + { + "name": "getSsl", + "parameterTypes": [] + }, + { + "name": "getTomcat", + "parameterTypes": [] + }, + { + "name": "setAddress", + "parameterTypes": [ + "java.net.InetAddress" + ] + }, + { + "name": "setForwardHeadersStrategy", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.ServerProperties$ForwardHeadersStrategy" + ] + }, + { + "name": "setPort", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setShutdown", + "parameterTypes": [ + "org.springframework.boot.web.server.Shutdown" + ] + }, + { + "name": "setSsl", + "parameterTypes": [ + "org.springframework.boot.web.server.Ssl" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.ServerProperties$Servlet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "getEncoding", + "parameterTypes": [] + }, + { + "name": "getJsp", + "parameterTypes": [] + }, + { + "name": "getSession", + "parameterTypes": [] + }, + { + "name": "setContextPath", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "getThreads", + "parameterTypes": [] + }, + { + "name": "setRedirectContextRoot", + "parameterTypes": [ + "java.lang.Boolean" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat$Threads", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"setMax","parameterTypes":["int"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.WebProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getResources", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.WebProperties$Resources", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"setStaticLocations","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration$NotReactiveWebApplicationCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer" +}, +{ + "name":"org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "tomcatWebServerFactoryCustomizer", + "parameterTypes": [ + "org.springframework.core.env.Environment", + "org.springframework.boot.autoconfigure.web.ServerProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "customize", + "parameterTypes": [ + "org.springframework.boot.web.server.WebServerFactory" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DefaultDispatcherServletCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "dispatcherServlet", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "dispatcherServletRegistration", + "parameterTypes": [ + "org.springframework.web.servlet.DispatcherServlet", + "org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getPrefix", + "parameterTypes": [] + }, + { + "name": "getRelativePath", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getServletUrlMapping", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addUrlMappings", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "getPath", + "parameterTypes": [] + }, + { + "name": "setUrlMappings", + "parameterTypes": [ + "java.util.Collection" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.ServerProperties" + ] + }, + { + "name": "characterEncodingFilter", + "parameterTypes": [] + }, + { + "name": "localeCharsetMappingsCustomizer", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration$LocaleCharsetMappingsCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "customize", + "parameterTypes": [ + "org.springframework.boot.web.server.WebServerFactory" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.servlet.MultipartProperties" + ] + }, + { + "name": "multipartConfigElement", + "parameterTypes": [] + }, + { + "name": "multipartResolver", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.MultipartProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setMaxFileSize", + "parameterTypes": [ + "org.springframework.util.unit.DataSize" + ] + }, + { + "name": "setMaxRequestSize", + "parameterTypes": [ + "org.springframework.util.unit.DataSize" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "servletWebServerFactoryCustomizer", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.ServerProperties", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "tomcatServletWebServerFactoryCustomizer", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.ServerProperties" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration$BeanPostProcessorsRegistrar", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedJetty" +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "tomcatServletWebServerFactory", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider", + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedUndertow" +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "customize", + "parameterTypes": [ + "org.springframework.boot.web.server.WebServerFactory" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.TomcatServletWebServerFactoryCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "customize", + "parameterTypes": [ + "org.springframework.boot.web.server.WebServerFactory" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration" +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "resolveErrorView", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "org.springframework.http.HttpStatus", + "java.util.Map" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.ServerProperties" + ] + }, + { + "name": "errorAttributes", + "parameterTypes": [] + }, + { + "name": "errorPageCustomizer", + "parameterTypes": [ + "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath" + ] + }, + { + "name": "preserveErrorControllerTargetClassPostProcessor", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.boot.autoconfigure.web.WebProperties" + ] + }, + { + "name": "conventionErrorViewResolver", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$ErrorPageCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "registerErrorPages", + "parameterTypes": [ + "org.springframework.boot.web.server.ErrorPageRegistry" + ] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$ErrorTemplateMissingCondition", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.autoconfigure.websocket.servlet.TomcatWebSocketServletWebServerCustomizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "customize", + "parameterTypes": [ + "org.springframework.boot.web.server.WebServerFactory" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration$WebSocketMessageConverterConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "com.fasterxml.jackson.databind.ObjectMapper" + ] + }, + { + "name": "configureMessageConverters", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "eagerStompWebSocketHandlerMapping", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "websocketServletWebServerCustomizer", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.availability.ApplicationAvailability", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getLivenessState", + "parameterTypes": [] + }, + { + "name": "getReadinessState", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.availability.ApplicationAvailabilityBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getLastChangeEvent", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getState", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getState", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.boot.availability.AvailabilityState" + ] + }, + { + "name": "onApplicationEvent", + "parameterTypes": [ + "org.springframework.context.ApplicationEvent" + ] + } + ] +}, +{ + "name":"org.springframework.boot.builder.ParentContextCloserApplicationListener", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.boot.logging.DeferredLogFactory"] }] +}, +{ + "name":"org.springframework.boot.cloud.CloudPlatform" +}, +{ + "name":"org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.ContextIdApplicationContextInitializer", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.FileEncodingApplicationListener", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.config.AnsiOutputApplicationListener", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.boot.logging.DeferredLogFactory","org.springframework.boot.ConfigurableBootstrapContext"] }] +}, +{ + "name":"org.springframework.boot.context.config.ConfigDataProperties", + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.springframework.boot.context.config.ConfigTreeConfigDataLoader", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.core.io.ResourceLoader"] }] +}, +{ + "name":"org.springframework.boot.context.config.DelegatingApplicationContextInitializer", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.config.DelegatingApplicationListener", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.config.StandardConfigDataLoader", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.config.StandardConfigDataLocationResolver", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.boot.logging.DeferredLogFactory","org.springframework.boot.context.properties.bind.Binder","org.springframework.core.io.ResourceLoader"] }] +}, +{ + "name":"org.springframework.boot.context.event.EventPublishingRunListener", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.boot.SpringApplication","java.lang.String[]"] }] +}, +{ + "name":"org.springframework.boot.context.logging.LoggingApplicationListener", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.properties.BoundConfigurationProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.ConfigurationProperties", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.ConfigurationPropertiesBinder$Factory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.properties.ConfigurationPropertiesBinding", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.properties.DeprecatedConfigurationProperty", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.EnableConfigurationProperties", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.context.properties.NestedConfigurationProperty", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.bind.Name", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.bind.Nested", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.context.properties.migrator.PropertiesMigrationListener", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.convert.DurationUnit", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.env.EnvironmentPostProcessorApplicationListener", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.env.PropertiesPropertySourceLoader", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.boot.logging.DeferredLogFactory"] }] +}, +{ + "name":"org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.env.YamlPropertySourceLoader", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.flyway.FlywayDatabaseInitializerDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.info.BuildProperties" +}, +{ + "name":"org.springframework.boot.info.GitProperties" +}, +{ + "name":"org.springframework.boot.jackson.JsonComponentModule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + } + ] +}, +{ + "name":"org.springframework.boot.jackson.JsonMixinModule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.jackson.JsonMixinModuleEntries", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.jdbc.SchemaManagementProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.jdbc.XADataSourceWrapper" +}, +{ + "name":"org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializerDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.liquibase.LiquibaseDatabaseInitializerDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.loader.LaunchedURLClassLoader", + "queryAllDeclaredMethods":true, + "methods":[{"name":"clearCache","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.logging.LoggingSystem" +}, +{ + "name":"org.springframework.boot.logging.java.JavaLoggingSystem$Factory", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.logging.java.JavaLoggingSystem.Factory" +}, +{ + "name":"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem$Factory", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory" +}, +{ + "name":"org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.logging.logback.LogbackLoggingSystem$Factory", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory" +}, +{ + "name":"org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.orm.jpa.JpaDatabaseInitializerDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.core.env.Environment"] }] +}, +{ + "name":"org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":["org.springframework.core.env.Environment"] }] +}, +{ + "name":"org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializerDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "initializeDatabase", + "parameterTypes": [] + }, + { + "name": "setResourceLoader", + "parameterTypes": [ + "org.springframework.core.io.ResourceLoader" + ] + } + ] +}, +{ + "name":"org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.task.TaskExecutorBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.task.TaskSchedulerBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.validation.beanvalidation.FilteredMethodValidationPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"afterPropertiesSet","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"byAnnotation","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.springframework.boot.web.client.RestTemplateBuilder" +}, +{ + "name":"org.springframework.boot.web.client.RestTemplateCustomizer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer", + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addConnectorCustomizers", + "parameterTypes": [ + "org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer[]" + ] + }, + { + "name": "addContextCustomizers", + "parameterTypes": [ + "org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer[]" + ] + }, + { + "name": "addEngineValves", + "parameterTypes": [ + "org.apache.catalina.Valve[]" + ] + }, + { + "name": "addProtocolHandlerCustomizers", + "parameterTypes": [ + "org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer[]" + ] + }, + { + "name": "getWebServer", + "parameterTypes": [ + "org.springframework.boot.web.servlet.ServletContextInitializer[]" + ] + }, + { + "name": "setBackgroundProcessorDelay", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setBaseDirectory", + "parameterTypes": [ + "java.io.File" + ] + }, + { + "name": "setResourceLoader", + "parameterTypes": [ + "org.springframework.core.io.ResourceLoader" + ] + }, + { + "name": "setUriEncoding", + "parameterTypes": [ + "java.nio.charset.Charset" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContextFactory", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.web.server.AbstractConfigurableWebServerFactory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addErrorPages", + "parameterTypes": [ + "org.springframework.boot.web.server.ErrorPage[]" + ] + }, + { + "name": "getAddress", + "parameterTypes": [] + }, + { + "name": "getCompression", + "parameterTypes": [] + }, + { + "name": "getErrorPages", + "parameterTypes": [] + }, + { + "name": "getHttp2", + "parameterTypes": [] + }, + { + "name": "getOrCreateSslStoreProvider", + "parameterTypes": [] + }, + { + "name": "getPort", + "parameterTypes": [] + }, + { + "name": "getServerHeader", + "parameterTypes": [] + }, + { + "name": "getShutdown", + "parameterTypes": [] + }, + { + "name": "getSsl", + "parameterTypes": [] + }, + { + "name": "getSslStoreProvider", + "parameterTypes": [] + }, + { + "name": "setAddress", + "parameterTypes": [ + "java.net.InetAddress" + ] + }, + { + "name": "setCompression", + "parameterTypes": [ + "org.springframework.boot.web.server.Compression" + ] + }, + { + "name": "setErrorPages", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setHttp2", + "parameterTypes": [ + "org.springframework.boot.web.server.Http2" + ] + }, + { + "name": "setPort", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setServerHeader", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setShutdown", + "parameterTypes": [ + "org.springframework.boot.web.server.Shutdown" + ] + }, + { + "name": "setSsl", + "parameterTypes": [ + "org.springframework.boot.web.server.Ssl" + ] + }, + { + "name": "setSslStoreProvider", + "parameterTypes": [ + "org.springframework.boot.web.server.SslStoreProvider" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.server.Compression", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"setEnabled","parameterTypes":["boolean"] }] +}, +{ + "name":"org.springframework.boot.web.server.ConfigurableWebServerFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.server.Cookie", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "setMaxAge", + "parameterTypes": [ + "java.time.Duration" + ] + }, + { + "name": "setPath", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.server.ErrorPageRegistrar", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.server.ErrorPageRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.server.Ssl", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setEnabled", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setKeyStore", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setKeyStorePassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setKeyStoreType", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.server.WebServerFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.server.WebServerFactoryCustomizer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.AbstractFilterRegistrationBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addServletNames", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "addServletRegistrationBeans", + "parameterTypes": [ + "org.springframework.boot.web.servlet.ServletRegistrationBean[]" + ] + }, + { + "name": "addUrlPatterns", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "getServletNames", + "parameterTypes": [] + }, + { + "name": "getServletRegistrationBeans", + "parameterTypes": [] + }, + { + "name": "getUrlPatterns", + "parameterTypes": [] + }, + { + "name": "isMatchAfter", + "parameterTypes": [] + }, + { + "name": "setDispatcherTypes", + "parameterTypes": [ + "jakarta.servlet.DispatcherType", + "jakarta.servlet.DispatcherType[]" + ] + }, + { + "name": "setDispatcherTypes", + "parameterTypes": [ + "java.util.EnumSet" + ] + }, + { + "name": "setMatchAfter", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setServletNames", + "parameterTypes": [ + "java.util.Collection" + ] + }, + { + "name": "setServletRegistrationBeans", + "parameterTypes": [ + "java.util.Collection" + ] + }, + { + "name": "setUrlPatterns", + "parameterTypes": [ + "java.util.Collection" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getFilter", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.DynamicRegistrationBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addInitParameter", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "configure", + "parameterTypes": [ + "jakarta.servlet.Registration$Dynamic" + ] + }, + { + "name": "getInitParameters", + "parameterTypes": [] + }, + { + "name": "isAsyncSupported", + "parameterTypes": [] + }, + { + "name": "setAsyncSupported", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setInitParameters", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.FilterRegistrationBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getFilter","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.web.servlet.RegistrationBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "isEnabled", + "parameterTypes": [] + }, + { + "name": "onStartup", + "parameterTypes": [ + "jakarta.servlet.ServletContext" + ] + }, + { + "name": "setEnabled", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setOrder", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.ServletContextInitializer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.ServletRegistrationBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getMultipartConfig", + "parameterTypes": [] + }, + { + "name": "getServlet", + "parameterTypes": [] + }, + { + "name": "getServletName", + "parameterTypes": [] + }, + { + "name": "getUrlMappings", + "parameterTypes": [] + }, + { + "name": "setLoadOnStartup", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setMultipartConfig", + "parameterTypes": [ + "jakarta.servlet.MultipartConfigElement" + ] + }, + { + "name": "setServlet", + "parameterTypes": [ + "jakarta.servlet.Servlet" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.WebListenerRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.context.ServletWebServerApplicationContextFactory", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.web.servlet.error.DefaultErrorAttributes", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getError", + "parameterTypes": [ + "org.springframework.web.context.request.WebRequest" + ] + }, + { + "name": "getErrorAttributes", + "parameterTypes": [ + "org.springframework.web.context.request.WebRequest", + "org.springframework.boot.web.error.ErrorAttributeOptions" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "resolveException", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object", + "java.lang.Exception" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.error.ErrorAttributes", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.error.ErrorController", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getOrder","parameterTypes":[] }] +}, +{ + "name":"org.springframework.boot.web.servlet.filter.OrderedFilter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addCookieSameSiteSuppliers", + "parameterTypes": [ + "org.springframework.boot.web.servlet.server.CookieSameSiteSupplier[]" + ] + }, + { + "name": "addInitializers", + "parameterTypes": [ + "org.springframework.boot.web.servlet.ServletContextInitializer[]" + ] + }, + { + "name": "addWebListeners", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "getContextPath", + "parameterTypes": [] + }, + { + "name": "getCookieSameSiteSuppliers", + "parameterTypes": [] + }, + { + "name": "getDisplayName", + "parameterTypes": [] + }, + { + "name": "getDocumentRoot", + "parameterTypes": [] + }, + { + "name": "getInitParameters", + "parameterTypes": [] + }, + { + "name": "getJsp", + "parameterTypes": [] + }, + { + "name": "getLocaleCharsetMappings", + "parameterTypes": [] + }, + { + "name": "getMimeMappings", + "parameterTypes": [] + }, + { + "name": "getSession", + "parameterTypes": [] + }, + { + "name": "isRegisterDefaultServlet", + "parameterTypes": [] + }, + { + "name": "setContextPath", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setCookieSameSiteSuppliers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setDisplayName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDocumentRoot", + "parameterTypes": [ + "java.io.File" + ] + }, + { + "name": "setInitParameters", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setInitializers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setJsp", + "parameterTypes": [ + "org.springframework.boot.web.servlet.server.Jsp" + ] + }, + { + "name": "setLocaleCharsetMappings", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setMimeMappings", + "parameterTypes": [ + "org.springframework.boot.web.server.MimeMappings" + ] + }, + { + "name": "setRegisterDefaultServlet", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setSession", + "parameterTypes": [ + "org.springframework.boot.web.servlet.server.Session" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.server.Encoding", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCharset", + "parameterTypes": [ + "java.nio.charset.Charset" + ] + }, + { + "name": "setForce", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.server.Jsp", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"setRegistered","parameterTypes":["boolean"] }] +}, +{ + "name":"org.springframework.boot.web.servlet.server.ServletWebServerFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.boot.web.servlet.server.Session", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "getCookie", + "parameterTypes": [] + }, + { + "name": "setTimeout", + "parameterTypes": [ + "java.time.Duration" + ] + } + ] +}, +{ + "name":"org.springframework.boot.web.servlet.server.Session$Cookie", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"org.springframework.cache.CacheManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.cache.annotation.AbstractCachingConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "setConfigurers", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "setImportMetadata", + "parameterTypes": [ + "org.springframework.core.type.AnnotationMetadata" + ] + } + ] +}, +{ + "name":"org.springframework.cache.annotation.AnnotationCacheOperationSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isCandidateClass", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.cache.annotation.Cacheable", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.cache.annotation.CachingConfigurationSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.cache.annotation.EnableCaching", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.cache.annotation.ProxyCachingConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "cacheAdvisor", + "parameterTypes": [ + "org.springframework.cache.interceptor.CacheOperationSource", + "org.springframework.cache.interceptor.CacheInterceptor" + ] + }, + { + "name": "cacheInterceptor", + "parameterTypes": [ + "org.springframework.cache.interceptor.CacheOperationSource" + ] + }, + { + "name": "cacheOperationSource", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.cache.caffeine.CaffeineCache" +}, +{ + "name":"org.springframework.cache.caffeine.CaffeineCacheManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getCache", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getCacheNames", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.cache.interceptor.AbstractCacheInvoker", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getErrorHandler", + "parameterTypes": [] + }, + { + "name": "setErrorHandler", + "parameterTypes": [ + "org.springframework.cache.interceptor.CacheErrorHandler" + ] + } + ] +}, +{ + "name":"org.springframework.cache.interceptor.AbstractFallbackCacheOperationSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getCacheOperations","parameterTypes":["java.lang.reflect.Method","java.lang.Class"] }] +}, +{ + "name":"org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getPointcut","parameterTypes":[] }] +}, +{ + "name":"org.springframework.cache.interceptor.CacheAspectSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "afterSingletonsInstantiated", + "parameterTypes": [] + }, + { + "name": "configure", + "parameterTypes": [ + "java.util.function.Supplier", + "java.util.function.Supplier", + "java.util.function.Supplier", + "java.util.function.Supplier" + ] + }, + { + "name": "getCacheOperationSource", + "parameterTypes": [] + }, + { + "name": "getCacheResolver", + "parameterTypes": [] + }, + { + "name": "getKeyGenerator", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "setCacheManager", + "parameterTypes": [ + "org.springframework.cache.CacheManager" + ] + }, + { + "name": "setCacheOperationSource", + "parameterTypes": [ + "org.springframework.cache.interceptor.CacheOperationSource" + ] + }, + { + "name": "setCacheOperationSources", + "parameterTypes": [ + "org.springframework.cache.interceptor.CacheOperationSource[]" + ] + }, + { + "name": "setCacheResolver", + "parameterTypes": [ + "org.springframework.cache.interceptor.CacheResolver" + ] + }, + { + "name": "setKeyGenerator", + "parameterTypes": [ + "org.springframework.cache.interceptor.KeyGenerator" + ] + } + ] +}, +{ + "name":"org.springframework.cache.interceptor.CacheInterceptor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"invoke","parameterTypes":["org.aopalliance.intercept.MethodInvocation"] }] +}, +{ + "name":"org.springframework.cache.interceptor.CacheOperationSource", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.cache.jcache.JCacheCache" +}, +{ + "name":"org.springframework.cache.jcache.config.ProxyJCacheConfiguration" +}, +{ + "name":"org.springframework.cache.transaction.TransactionAwareCacheDecorator" +}, +{ + "name":"org.springframework.cglib.proxy.Factory", + "queryAllDeclaredMethods": true +}, +{ + "name":"org.springframework.context.ApplicationContextAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.ApplicationEventPublisherAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.ApplicationListener", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.ApplicationStartupAware", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.EmbeddedValueResolverAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.EnvironmentAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.Lifecycle", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.LifecycleProcessor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.MessageSourceAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.Phased", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.ResourceLoaderAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.SmartLifecycle", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getPhase", + "parameterTypes": [] + }, + { + "name": "isAutoStartup", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [ + "java.lang.Runnable" + ] + } + ] +}, +{ + "name":"org.springframework.context.annotation.AnnotationScopeMetadataResolver", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.context.annotation.AutoProxyRegistrar", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.context.annotation.Bean", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.CommonAnnotationBeanPostProcessor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.context.annotation.ComponentScan", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.ComponentScan$Filter", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.Conditional", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.Configuration", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.ConfigurationClassEnhancer$EnhancedConfiguration", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.annotation.ConfigurationClassParser$DefaultDeferredImportSelectorGroup", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.context.annotation.ConfigurationClassPostProcessor", + "allDeclaredFields":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setMetadataReaderFactory", + "parameterTypes": [ + "org.springframework.core.type.classreading.MetadataReaderFactory" + ] + } + ] +}, +{ + "name":"org.springframework.context.annotation.DependsOn", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.Import", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.ImportAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.context.annotation.ImportRuntimeHints", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.Lazy", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.Primary", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.Role", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.annotation.Scope", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.event.DefaultEventListenerFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.context.event.EventListener", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.context.event.EventListenerMethodProcessor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.context.event.SmartApplicationListener", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getListenerId", + "parameterTypes": [] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "supportsSourceType", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.context.support.ApplicationObjectSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getApplicationContext", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.context.support.DefaultLifecycleProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "isRunning", + "parameterTypes": [] + }, + { + "name": "onClose", + "parameterTypes": [] + }, + { + "name": "onRefresh", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "start", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.context.support.PropertySourcesPlaceholderConfigurer" +}, +{ + "name":"org.springframework.context.weaving.LoadTimeWeaverAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.DecoratingProxy", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.Ordered", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.PriorityOrdered", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.annotation.AliasFor", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.core.annotation.Order", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.core.convert.ConversionService", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.convert.converter.ConverterRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.convert.converter.GenericConverter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.convert.support.ConfigurableConversionService", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.convert.support.GenericConversionService", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addConverter", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Class", + "org.springframework.core.convert.converter.Converter" + ] + }, + { + "name": "addConverter", + "parameterTypes": [ + "org.springframework.core.convert.converter.Converter" + ] + }, + { + "name": "addConverter", + "parameterTypes": [ + "org.springframework.core.convert.converter.GenericConverter" + ] + }, + { + "name": "addConverterFactory", + "parameterTypes": [ + "org.springframework.core.convert.converter.ConverterFactory" + ] + }, + { + "name": "canBypassConvert", + "parameterTypes": [ + "org.springframework.core.convert.TypeDescriptor", + "org.springframework.core.convert.TypeDescriptor" + ] + }, + { + "name": "canConvert", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Class" + ] + }, + { + "name": "canConvert", + "parameterTypes": [ + "org.springframework.core.convert.TypeDescriptor", + "org.springframework.core.convert.TypeDescriptor" + ] + }, + { + "name": "convert", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Class" + ] + }, + { + "name": "convert", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.core.convert.TypeDescriptor" + ] + }, + { + "name": "convert", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.core.convert.TypeDescriptor", + "org.springframework.core.convert.TypeDescriptor" + ] + }, + { + "name": "removeConvertible", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Class" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.core.env.EnvironmentCapable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.io.Resource", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.core.io.support.PropertiesLoaderSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "setFileEncoding", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setIgnoreResourceNotFound", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setLocalOverride", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setLocation", + "parameterTypes": [ + "org.springframework.core.io.Resource" + ] + }, + { + "name": "setLocations", + "parameterTypes": [ + "org.springframework.core.io.Resource[]" + ] + }, + { + "name": "setProperties", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setPropertiesArray", + "parameterTypes": [ + "java.util.Properties[]" + ] + }, + { + "name": "setPropertiesPersister", + "parameterTypes": [ + "org.springframework.util.PropertiesPersister" + ] + } + ] +}, +{ + "name":"org.springframework.core.task.AsyncListenableTaskExecutor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.task.AsyncTaskExecutor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "submitCompletable", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "submitCompletable", + "parameterTypes": [ + "java.util.concurrent.Callable" + ] + } + ] +}, +{ + "name":"org.springframework.core.task.TaskExecutor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.core.type.classreading.MetadataReaderFactory", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setBeanFactory","parameterTypes":["org.springframework.beans.factory.BeanFactory"] }] +}, +{ + "name":"org.springframework.dao.support.PersistenceExceptionTranslator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.annotation.QueryAnnotation", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.cassandra.ReactiveSession" +}, +{ + "name":"org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate" +}, +{ + "name":"org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient" +}, +{ + "name":"org.springframework.data.elasticsearch.repository.ElasticsearchRepository" +}, +{ + "name":"org.springframework.data.envers.repository.config$EnableEnversRepositories" +}, +{ + "name":"org.springframework.data.envers.repository.config.EnableEnversRepositories" +}, +{ + "name":"org.springframework.data.geo.GeoModule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration" +}, +{ + "name":"org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters$InstantConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "convertToDatabaseColumn", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "convertToEntityAttribute", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.data.jpa.mapping.JpaMetamodelMappingContext", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "findPersistentPropertyPaths", + "parameterTypes": [ + "java.lang.Class", + "java.util.function.Predicate" + ] + }, + { + "name": "hasPersistentEntityFor", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.data.jpa.repository.JpaContext", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.jpa.repository.JpaRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "deleteAllByIdInBatch", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteAllInBatch", + "parameterTypes": [] + }, + { + "name": "deleteAllInBatch", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteInBatch", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Example", + "org.springframework.data.domain.Sort" + ] + }, + { + "name": "flush", + "parameterTypes": [] + }, + { + "name": "getById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getOne", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getReferenceById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "saveAllAndFlush", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "saveAndFlush", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.data.jpa.repository.JpaSpecificationExecutor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.jpa.repository.Modifying", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.jpa.repository.Query", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "countName", + "parameterTypes": [] + }, + { + "name": "countProjection", + "parameterTypes": [] + }, + { + "name": "countQuery", + "parameterTypes": [] + }, + { + "name": "name", + "parameterTypes": [] + }, + { + "name": "nativeQuery", + "parameterTypes": [] + }, + { + "name": "queryRewriter", + "parameterTypes": [] + }, + { + "name": "value", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.jpa.repository.config.EnableJpaRepositories", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.jpa.repository.config.JpaMetamodelMappingContextFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "getObjectType", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension" +}, +{ + "name":"org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension$JpaRepositoryRegistrationAotProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.data.jpa.repository.support.CrudMethodMetadata", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getComment", + "parameterTypes": [] + }, + { + "name": "getEntityGraph", + "parameterTypes": [] + }, + { + "name": "getLockModeType", + "parameterTypes": [] + }, + { + "name": "getQueryHints", + "parameterTypes": [] + }, + { + "name": "getQueryHintsForCount", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.jpa.repository.support.DefaultJpaContext", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "char" + ] + }, + { + "name": "getExtensionId", + "parameterTypes": [] + }, + { + "name": "getRootObject", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "setEntityManager", + "parameterTypes": [ + "jakarta.persistence.EntityManager" + ] + }, + { + "name": "setEntityPathResolver", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "setEscapeCharacter", + "parameterTypes": [ + "char" + ] + }, + { + "name": "setMappingContext", + "parameterTypes": [ + "org.springframework.data.mapping.context.MappingContext" + ] + }, + { + "name": "setQueryMethodFactory", + "parameterTypes": [ + "org.springframework.data.jpa.repository.query.JpaQueryMethodFactory" + ] + } + ] +}, +{ + "name":"org.springframework.data.jpa.repository.support.JpaRepositoryImplementation", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.jpa.repository.support.SimpleJpaRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.data.jpa.repository.support.JpaEntityInformation", + "jakarta.persistence.EntityManager" + ] + }, + { + "name": "count", + "parameterTypes": [] + }, + { + "name": "count", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + }, + { + "name": "count", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification" + ] + }, + { + "name": "delete", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "delete", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification" + ] + }, + { + "name": "deleteAll", + "parameterTypes": [] + }, + { + "name": "deleteAll", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteAllById", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteAllByIdInBatch", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteAllInBatch", + "parameterTypes": [] + }, + { + "name": "deleteAllInBatch", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "exists", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + }, + { + "name": "exists", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification" + ] + }, + { + "name": "existsById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "findAll", + "parameterTypes": [] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Example", + "org.springframework.data.domain.Pageable" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Example", + "org.springframework.data.domain.Sort" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Pageable" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Sort" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification", + "org.springframework.data.domain.Pageable" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification", + "org.springframework.data.domain.Sort" + ] + }, + { + "name": "findAllById", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "findBy", + "parameterTypes": [ + "org.springframework.data.domain.Example", + "java.util.function.Function" + ] + }, + { + "name": "findBy", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification", + "java.util.function.Function" + ] + }, + { + "name": "findById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "findOne", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + }, + { + "name": "findOne", + "parameterTypes": [ + "org.springframework.data.jpa.domain.Specification" + ] + }, + { + "name": "flush", + "parameterTypes": [] + }, + { + "name": "getById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getOne", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getReferenceById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "save", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "saveAll", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "saveAllAndFlush", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "saveAndFlush", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setEscapeCharacter", + "parameterTypes": [ + "org.springframework.data.jpa.repository.query.EscapeCharacter" + ] + }, + { + "name": "setRepositoryMethodMetadata", + "parameterTypes": [ + "org.springframework.data.jpa.repository.support.CrudMethodMetadata" + ] + } + ] +}, +{ + "name":"org.springframework.data.jpa.util.HibernateProxyDetector", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.data.jpa.util.JpaMetamodelCacheCleanup", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "destroy", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.ldap.repository.LdapRepository" +}, +{ + "name":"org.springframework.data.mapping.PersistentPropertyAccessor" +}, +{ + "name":"org.springframework.data.mapping.context.AbstractMappingContext", + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "createPersistentProperty", + "parameterTypes": [ + "org.springframework.data.mapping.model.Property", + "org.springframework.data.mapping.model.MutablePersistentEntity", + "org.springframework.data.mapping.model.SimpleTypeHolder" + ] + }, + { + "name": "getManagedTypes", + "parameterTypes": [] + }, + { + "name": "getPersistentEntities", + "parameterTypes": [] + }, + { + "name": "getPersistentEntity", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getPersistentEntity", + "parameterTypes": [ + "org.springframework.data.mapping.PersistentProperty" + ] + }, + { + "name": "getPersistentEntity", + "parameterTypes": [ + "org.springframework.data.util.TypeInformation" + ] + }, + { + "name": "getPersistentPropertyPath", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "getPersistentPropertyPath", + "parameterTypes": [ + "org.springframework.data.mapping.PropertyPath" + ] + }, + { + "name": "initialize", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setApplicationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + }, + { + "name": "setInitialEntitySet", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setManagedTypes", + "parameterTypes": [ + "org.springframework.data.domain.ManagedTypes" + ] + }, + { + "name": "setSimpleTypeHolder", + "parameterTypes": [ + "org.springframework.data.mapping.model.SimpleTypeHolder" + ] + }, + { + "name": "setStrict", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.data.mapping.context.MappingContext", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getPersistentEntity", + "parameterTypes": [ + "org.springframework.data.mapping.PersistentProperty" + ] + }, + { + "name": "getPersistentEntity", + "parameterTypes": [ + "org.springframework.data.util.TypeInformation" + ] + }, + { + "name": "getRequiredPersistentEntity", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getRequiredPersistentEntity", + "parameterTypes": [ + "org.springframework.data.mapping.PersistentProperty" + ] + }, + { + "name": "getRequiredPersistentEntity", + "parameterTypes": [ + "org.springframework.data.util.TypeInformation" + ] + } + ] +}, +{ + "name":"org.springframework.data.mongodb.core.MongoTemplate" +}, +{ + "name":"org.springframework.data.querydsl.QuerydslPredicateExecutor" +}, +{ + "name":"org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor" +}, +{ + "name":"org.springframework.data.r2dbc.core.R2dbcEntityTemplate" +}, +{ + "name":"org.springframework.data.redis.cache.RedisCache" +}, +{ + "name":"org.springframework.data.redis.connection.RedisConnectionFactory" +}, +{ + "name":"org.springframework.data.redis.core.RedisOperations" +}, +{ + "name":"org.springframework.data.redis.repository.configuration.EnableRedisRepositories" +}, +{ + "name":"org.springframework.data.repository.CrudRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "count", + "parameterTypes": [] + }, + { + "name": "delete", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "deleteAll", + "parameterTypes": [] + }, + { + "name": "deleteAll", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteAllById", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "deleteById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "existsById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "findById", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "save", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.ListCrudRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "findAll", + "parameterTypes": [] + }, + { + "name": "findAllById", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "saveAll", + "parameterTypes": [ + "java.lang.Iterable" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.ListPagingAndSortingRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"findAll","parameterTypes":["org.springframework.data.domain.Sort"] }] +}, +{ + "name":"org.springframework.data.repository.NoRepositoryBean", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.repository.PagingAndSortingRepository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"findAll","parameterTypes":["org.springframework.data.domain.Pageable"] }] +}, +{ + "name":"org.springframework.data.repository.Repository", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.repository.config.PropertiesBasedNamedQueriesFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getObject", + "parameterTypes": [] + }, + { + "name": "getObjectType", + "parameterTypes": [] + }, + { + "name": "isSingleton", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.repository.config.RepositoryConfiguration", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.repository.config.RepositoryRegistrationAotProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getConfigMap", + "parameterTypes": [] + }, + { + "name": "processAheadOfTime", + "parameterTypes": [ + "org.springframework.beans.factory.support.RegisteredBean" + ] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "setConfigMap", + "parameterTypes": [ + "java.util.Map" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.core.NamedQueries", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.repository.core.support.PropertiesBasedNamedQueries", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "hasQuery", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "empty", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "iterator", + "parameterTypes": [] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addRepositoryFactoryCustomizer", + "parameterTypes": [ + "org.springframework.data.repository.core.support.RepositoryFactoryCustomizer" + ] + }, + { + "name": "getEntityInformation", + "parameterTypes": [] + }, + { + "name": "getObject", + "parameterTypes": [] + }, + { + "name": "getObjectType", + "parameterTypes": [] + }, + { + "name": "getPersistentEntity", + "parameterTypes": [] + }, + { + "name": "getQueryMethods", + "parameterTypes": [] + }, + { + "name": "getRepositoryInformation", + "parameterTypes": [] + }, + { + "name": "isSingleton", + "parameterTypes": [] + }, + { + "name": "setApplicationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setCustomImplementation", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setEvaluationContextProvider", + "parameterTypes": [ + "org.springframework.data.repository.query.QueryMethodEvaluationContextProvider" + ] + }, + { + "name": "setLazyInit", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setNamedQueries", + "parameterTypes": [ + "org.springframework.data.repository.core.NamedQueries" + ] + }, + { + "name": "setQueryLookupStrategyKey", + "parameterTypes": [ + "org.springframework.data.repository.query.QueryLookupStrategy$Key" + ] + }, + { + "name": "setRepositoryBaseClass", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setRepositoryFragments", + "parameterTypes": [ + "org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.core.support.RepositoryFactoryInformation", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getObject", + "parameterTypes": [] + }, + { + "name": "getObjectType", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.core.support.RepositoryMethodInvocationListener", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "setEnableDefaultTransactions", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setTransactionManager", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.query.Param" +}, +{ + "name":"org.springframework.data.repository.query.QueryByExampleExecutor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "count", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + }, + { + "name": "exists", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + }, + { + "name": "findAll", + "parameterTypes": [ + "org.springframework.data.domain.Example", + "org.springframework.data.domain.Pageable" + ] + }, + { + "name": "findBy", + "parameterTypes": [ + "org.springframework.data.domain.Example", + "java.util.function.Function" + ] + }, + { + "name": "findOne", + "parameterTypes": [ + "org.springframework.data.domain.Example" + ] + } + ] +}, +{ + "name":"org.springframework.data.repository.query.QueryLookupStrategy$Key", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.repository.query.ReactiveQueryByExampleExecutor" +}, +{ + "name":"org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" +}, +{ + "name":"org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration" +}, +{ + "name":"org.springframework.data.spel.spi.EvaluationContextExtension", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getFunctions", + "parameterTypes": [] + }, + { + "name": "getProperties", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.spel.spi.ExtensionIdAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.util.CustomCollections$EclipseCollections", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.data.util.CustomCollections$VavrCollections", + "queryAllDeclaredConstructors":true, + "queryAllPublicConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.data.util.CustomCollections.EclipseCollections" +}, +{ + "name":"org.springframework.data.util.CustomCollections.VavrCollections" +}, +{ + "name":"org.springframework.data.util.Streamable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "and", + "parameterTypes": [ + "java.lang.Iterable" + ] + }, + { + "name": "and", + "parameterTypes": [ + "java.util.function.Supplier" + ] + }, + { + "name": "and", + "parameterTypes": [ + "org.springframework.data.util.Streamable" + ] + }, + { + "name": "and", + "parameterTypes": [ + "java.lang.Object[]" + ] + }, + { + "name": "filter", + "parameterTypes": [ + "java.util.function.Predicate" + ] + }, + { + "name": "flatMap", + "parameterTypes": [ + "java.util.function.Function" + ] + }, + { + "name": "get", + "parameterTypes": [] + }, + { + "name": "isEmpty", + "parameterTypes": [] + }, + { + "name": "map", + "parameterTypes": [ + "java.util.function.Function" + ] + }, + { + "name": "stream", + "parameterTypes": [] + }, + { + "name": "toList", + "parameterTypes": [] + }, + { + "name": "toSet", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.web.PageableArgumentResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.web.PageableHandlerMethodArgumentResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "resolveArgument", + "parameterTypes": [ + "org.springframework.core.MethodParameter", + "org.springframework.web.method.support.ModelAndViewContainer", + "org.springframework.web.context.request.NativeWebRequest", + "org.springframework.web.bind.support.WebDataBinderFactory" + ] + }, + { + "name": "supportsParameter", + "parameterTypes": [ + "org.springframework.core.MethodParameter" + ] + } + ] +}, +{ + "name":"org.springframework.data.web.PageableHandlerMethodArgumentResolverSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "isFallbackPageable", + "parameterTypes": [ + "org.springframework.data.domain.Pageable" + ] + }, + { + "name": "setFallbackPageable", + "parameterTypes": [ + "org.springframework.data.domain.Pageable" + ] + }, + { + "name": "setMaxPageSize", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setOneIndexedParameters", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setPageParameterName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPrefix", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setQualifierDelimiter", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSizeParameterName", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.data.web.SortArgumentResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.web.SortHandlerMethodArgumentResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "resolveArgument", + "parameterTypes": [ + "org.springframework.core.MethodParameter", + "org.springframework.web.method.support.ModelAndViewContainer", + "org.springframework.web.context.request.NativeWebRequest", + "org.springframework.web.bind.support.WebDataBinderFactory" + ] + }, + { + "name": "supportsParameter", + "parameterTypes": [ + "org.springframework.core.MethodParameter" + ] + } + ] +}, +{ + "name":"org.springframework.data.web.SortHandlerMethodArgumentResolverSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getPropertyDelimiter", + "parameterTypes": [] + }, + { + "name": "setFallbackSort", + "parameterTypes": [ + "org.springframework.data.domain.Sort" + ] + }, + { + "name": "setPropertyDelimiter", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setQualifierDelimiter", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setSortParameter", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.data.web.config.EnableSpringDataWebSupport", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.data.web.config.EnableSpringDataWebSupport$QuerydslActivator", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.data.web.config.EnableSpringDataWebSupport$SpringDataWebConfigurationImportSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.data.web.config.PageableHandlerMethodArgumentResolverCustomizer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.web.config.ProjectingArgumentResolverRegistrar", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "projectingArgumentResolverBeanPostProcessor", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectFactory" + ] + } + ] +}, +{ + "name":"org.springframework.data.web.config.ProjectingArgumentResolverRegistrar$ProjectingArgumentResolverBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + } + ] +}, +{ + "name":"org.springframework.data.web.config.SortHandlerMethodArgumentResolverCustomizer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.web.config.SpringDataJacksonConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "jacksonGeoModule", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.data.web.config.SpringDataJacksonModules", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.data.web.config.SpringDataWebConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "org.springframework.context.ApplicationContext", + "org.springframework.beans.factory.ObjectFactory" + ] + }, + { + "name": "addArgumentResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "addFormatters", + "parameterTypes": [ + "org.springframework.format.FormatterRegistry" + ] + }, + { + "name": "extendMessageConverters", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "pageableResolver", + "parameterTypes": [] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "sortResolver", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.format.FormatterRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.format.support.DefaultFormattingConversionService", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.format.support.FormattingConversionService", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addFormatter", + "parameterTypes": [ + "org.springframework.format.Formatter" + ] + }, + { + "name": "addFormatterForFieldAnnotation", + "parameterTypes": [ + "org.springframework.format.AnnotationFormatterFactory" + ] + }, + { + "name": "addFormatterForFieldType", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.format.Formatter" + ] + }, + { + "name": "addFormatterForFieldType", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.format.Printer", + "org.springframework.format.Parser" + ] + }, + { + "name": "addParser", + "parameterTypes": [ + "org.springframework.format.Parser" + ] + }, + { + "name": "addPrinter", + "parameterTypes": [ + "org.springframework.format.Printer" + ] + }, + { + "name": "setEmbeddedValueResolver", + "parameterTypes": [ + "org.springframework.util.StringValueResolver" + ] + } + ] +}, +{ + "name":"org.springframework.hateoas$Link" +}, +{ + "name":"org.springframework.hateoas.EntityModel" +}, +{ + "name":"org.springframework.hateoas.Link" +}, +{ + "name":"org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter" +}, +{ + "name":"org.springframework.http.HttpStatus" +}, +{ + "name":"org.springframework.http.ReactiveHttpInputMessage" +}, +{ + "name":"org.springframework.http.client.ClientHttpRequestFactory", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.http.client.support.HttpAccessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getClientHttpRequestInitializers", + "parameterTypes": [] + }, + { + "name": "setClientHttpRequestInitializers", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.springframework.http.client.support.InterceptingHttpAccessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getInterceptors", + "parameterTypes": [] + }, + { + "name": "getRequestFactory", + "parameterTypes": [] + }, + { + "name": "setInterceptors", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setRequestFactory", + "parameterTypes": [ + "org.springframework.http.client.ClientHttpRequestFactory" + ] + } + ] +}, +{ + "name":"org.springframework.http.codec.CodecConfigurer" +}, +{ + "name":"org.springframework.http.codec.multipart.DefaultPartHttpMessageReader" +}, +{ + "name":"org.springframework.http.converter.AbstractGenericHttpMessageConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "canWrite", + "parameterTypes": [ + "java.lang.reflect.Type", + "java.lang.Class", + "org.springframework.http.MediaType" + ] + }, + { + "name": "write", + "parameterTypes": [ + "java.lang.Object", + "java.lang.reflect.Type", + "org.springframework.http.MediaType", + "org.springframework.http.HttpOutputMessage" + ] + } + ] +}, +{ + "name":"org.springframework.http.converter.AbstractHttpMessageConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addDefaultHeaders", + "parameterTypes": [ + "org.springframework.http.HttpHeaders", + "java.lang.Object", + "org.springframework.http.MediaType" + ] + }, + { + "name": "canRead", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.http.MediaType" + ] + }, + { + "name": "canWrite", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.http.MediaType" + ] + }, + { + "name": "getContentLength", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.http.MediaType" + ] + }, + { + "name": "getDefaultCharset", + "parameterTypes": [] + }, + { + "name": "getSupportedMediaTypes", + "parameterTypes": [] + }, + { + "name": "read", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.http.HttpInputMessage" + ] + }, + { + "name": "setDefaultCharset", + "parameterTypes": [ + "java.nio.charset.Charset" + ] + }, + { + "name": "setSupportedMediaTypes", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "write", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.http.MediaType", + "org.springframework.http.HttpOutputMessage" + ] + }, + { + "name": "writeInternal", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.http.HttpOutputMessage" + ] + } + ] +}, +{ + "name":"org.springframework.http.converter.GenericHttpMessageConverter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.http.converter.HttpMessageConverter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getSupportedMediaTypes","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.springframework.http.converter.StringHttpMessageConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "canRead", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.http.MediaType" + ] + }, + { + "name": "canRead", + "parameterTypes": [ + "java.lang.reflect.Type", + "java.lang.Class", + "org.springframework.http.MediaType" + ] + }, + { + "name": "canWrite", + "parameterTypes": [ + "java.lang.Class", + "org.springframework.http.MediaType" + ] + }, + { + "name": "getObjectMapper", + "parameterTypes": [] + }, + { + "name": "getObjectMappersForType", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getSupportedMediaTypes", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "read", + "parameterTypes": [ + "java.lang.reflect.Type", + "java.lang.Class", + "org.springframework.http.HttpInputMessage" + ] + }, + { + "name": "registerObjectMappersForType", + "parameterTypes": [ + "java.lang.Class", + "java.util.function.Consumer" + ] + }, + { + "name": "setObjectMapper", + "parameterTypes": [ + "com.fasterxml.jackson.databind.ObjectMapper" + ] + }, + { + "name": "setPrettyPrint", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setSupportedMediaTypes", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.springframework.http.converter.json.GsonHttpMessageConverter" +}, +{ + "name":"org.springframework.http.converter.json.Jackson2ObjectMapperBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.http.converter.json.MappingJackson2HttpMessageConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.http.server.reactive.HttpHandler" +}, +{ + "name":"org.springframework.instrument$InstrumentationSavingAgent" +}, +{ + "name":"org.springframework.instrument.InstrumentationSavingAgent" +}, +{ + "name":"org.springframework.integration.config.EnableIntegration" +}, +{ + "name":"org.springframework.integration.graph.IntegrationGraphServer" +}, +{ + "name":"org.springframework.jdbc.core.JdbcOperations", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.jdbc.core.JdbcTemplate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "batchUpdate", + "parameterTypes": [ + "java.lang.String", + "java.util.Collection", + "int", + "org.springframework.jdbc.core.ParameterizedPreparedStatementSetter" + ] + }, + { + "name": "batchUpdate", + "parameterTypes": [ + "java.lang.String", + "java.util.List" + ] + }, + { + "name": "batchUpdate", + "parameterTypes": [ + "java.lang.String", + "java.util.List", + "int[]" + ] + }, + { + "name": "batchUpdate", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.BatchPreparedStatementSetter" + ] + }, + { + "name": "batchUpdate", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "call", + "parameterTypes": [ + "org.springframework.jdbc.core.CallableStatementCreator", + "java.util.List" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.CallableStatementCallback" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.PreparedStatementCallback" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "org.springframework.jdbc.core.CallableStatementCreator", + "org.springframework.jdbc.core.CallableStatementCallback" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "org.springframework.jdbc.core.ConnectionCallback" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "org.springframework.jdbc.core.PreparedStatementCreator", + "org.springframework.jdbc.core.PreparedStatementCallback" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "org.springframework.jdbc.core.StatementCallback" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.PreparedStatementSetter", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.PreparedStatementSetter", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.PreparedStatementSetter", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.ResultSetExtractor", + "java.lang.Object[]" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowCallbackHandler", + "java.lang.Object[]" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowMapper", + "java.lang.Object[]" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "query", + "parameterTypes": [ + "org.springframework.jdbc.core.PreparedStatementCreator", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "org.springframework.jdbc.core.PreparedStatementCreator", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "org.springframework.jdbc.core.PreparedStatementCreator", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "java.lang.Class" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]", + "java.lang.Class" + ] + }, + { + "name": "queryForMap", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "queryForMap", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]" + ] + }, + { + "name": "queryForMap", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowMapper", + "java.lang.Object[]" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "java.lang.Class" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]", + "java.lang.Class" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForRowSet", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "queryForRowSet", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]" + ] + }, + { + "name": "queryForRowSet", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]" + ] + }, + { + "name": "queryForStream", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.PreparedStatementSetter", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForStream", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForStream", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowMapper", + "java.lang.Object[]" + ] + }, + { + "name": "queryForStream", + "parameterTypes": [ + "org.springframework.jdbc.core.PreparedStatementCreator", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.PreparedStatementSetter" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]", + "int[]" + ] + }, + { + "name": "update", + "parameterTypes": [ + "org.springframework.jdbc.core.PreparedStatementCreator" + ] + }, + { + "name": "update", + "parameterTypes": [ + "org.springframework.jdbc.core.PreparedStatementCreator", + "org.springframework.jdbc.support.KeyHolder" + ] + } + ] +}, +{ + "name":"org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "batchUpdate", + "parameterTypes": [ + "java.lang.String", + "java.util.Map[]" + ] + }, + { + "name": "batchUpdate", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource[]" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "org.springframework.jdbc.core.PreparedStatementCallback" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.PreparedStatementCallback" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.core.PreparedStatementCallback" + ] + }, + { + "name": "getJdbcOperations", + "parameterTypes": [] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.core.ResultSetExtractor" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.core.RowCallbackHandler" + ] + }, + { + "name": "query", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "java.lang.Class" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource" + ] + }, + { + "name": "queryForList", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "java.lang.Class" + ] + }, + { + "name": "queryForMap", + "parameterTypes": [ + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "queryForMap", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "java.lang.Class" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "java.lang.Class" + ] + }, + { + "name": "queryForObject", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForRowSet", + "parameterTypes": [ + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "queryForRowSet", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource" + ] + }, + { + "name": "queryForStream", + "parameterTypes": [ + "java.lang.String", + "java.util.Map", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "queryForStream", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.core.RowMapper" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.support.KeyHolder" + ] + }, + { + "name": "update", + "parameterTypes": [ + "java.lang.String", + "org.springframework.jdbc.core.namedparam.SqlParameterSource", + "org.springframework.jdbc.support.KeyHolder", + "java.lang.String[]" + ] + } + ] +}, +{ + "name":"org.springframework.jdbc.datasource.DelegatingDataSource" +}, +{ + "name":"org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType" +}, +{ + "name":"org.springframework.jdbc.datasource.init.DatabasePopulator" +}, +{ + "name":"org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource" +}, +{ + "name":"org.springframework.jdbc.support.JdbcAccessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getDataSource", + "parameterTypes": [] + }, + { + "name": "getExceptionTranslator", + "parameterTypes": [] + }, + { + "name": "isLazyInit", + "parameterTypes": [] + }, + { + "name": "setDataSource", + "parameterTypes": [ + "javax.sql.DataSource" + ] + }, + { + "name": "setDatabaseProductName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setExceptionTranslator", + "parameterTypes": [ + "org.springframework.jdbc.support.SQLExceptionTranslator" + ] + }, + { + "name": "setLazyInit", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.jdbc.support.JdbcUtils" +}, +{ + "name":"org.springframework.jms.core.JmsTemplate" +}, +{ + "name":"org.springframework.jmx.export.MBeanExporter" +}, +{ + "name":"org.springframework.jndi.JndiObjectFactoryBean" +}, +{ + "name":"org.springframework.kafka.core.KafkaTemplate" +}, +{ + "name":"org.springframework.kafka.core.ProducerFactory" +}, +{ + "name":"org.springframework.lang.NonNullApi" +}, +{ + "name":"org.springframework.lang.Nullable" +}, +{ + "name":"org.springframework.ldap.core.ContextSource" +}, +{ + "name":"org.springframework.ldap.core.LdapOperations" +}, +{ + "name":"org.springframework.mail.javamail.JavaMailSenderImpl" +}, +{ + "name":"org.springframework.messaging.MessageChannel", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.MessageHandler", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.SubscribableChannel", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.converter.CompositeMessageConverter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "fromMessage", + "parameterTypes": [ + "org.springframework.messaging.Message", + "java.lang.Class" + ] + }, + { + "name": "fromMessage", + "parameterTypes": [ + "org.springframework.messaging.Message", + "java.lang.Class", + "java.lang.Object" + ] + }, + { + "name": "toMessage", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.messaging.MessageHeaders" + ] + }, + { + "name": "toMessage", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.messaging.MessageHeaders", + "java.lang.Object" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.messaging.converter.MessageConverter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.converter.SmartMessageConverter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.core.AbstractMessageSendingTemplate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "convertAndSend", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "convertAndSend", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Object" + ] + }, + { + "name": "convertAndSend", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Object", + "java.util.Map" + ] + }, + { + "name": "convertAndSend", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Object", + "java.util.Map", + "org.springframework.messaging.core.MessagePostProcessor" + ] + }, + { + "name": "convertAndSend", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Object", + "org.springframework.messaging.core.MessagePostProcessor" + ] + }, + { + "name": "convertAndSend", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.messaging.core.MessagePostProcessor" + ] + }, + { + "name": "doSend", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.messaging.Message" + ] + }, + { + "name": "getDefaultDestination", + "parameterTypes": [] + }, + { + "name": "getMessageConverter", + "parameterTypes": [] + }, + { + "name": "send", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.messaging.Message" + ] + }, + { + "name": "setDefaultDestination", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setMessageConverter", + "parameterTypes": [ + "org.springframework.messaging.converter.MessageConverter" + ] + } + ] +}, +{ + "name":"org.springframework.messaging.core.MessageSendingOperations", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.handler.annotation.MessageMapping", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.messaging.handler.annotation.SendTo", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver", + "methods":[{"name":"noMatchingExceptionHandler","parameterTypes":[] }] +}, +{ + "name":"org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getApplicationContext", + "parameterTypes": [] + }, + { + "name": "getArgumentResolvers", + "parameterTypes": [] + }, + { + "name": "getCustomArgumentResolvers", + "parameterTypes": [] + }, + { + "name": "getCustomReturnValueHandlers", + "parameterTypes": [] + }, + { + "name": "getDestinationPrefixes", + "parameterTypes": [] + }, + { + "name": "getDirectLookupDestinations", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getHandlerMethods", + "parameterTypes": [] + }, + { + "name": "getMatchingMapping", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.messaging.Message" + ] + }, + { + "name": "getReturnValueHandlers", + "parameterTypes": [] + }, + { + "name": "handleMatch", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.messaging.handler.HandlerMethod", + "java.lang.String", + "org.springframework.messaging.Message" + ] + }, + { + "name": "handleMessage", + "parameterTypes": [ + "org.springframework.messaging.Message" + ] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setArgumentResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setCustomArgumentResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setCustomReturnValueHandlers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setReturnValueHandlers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler" +}, +{ + "name":"org.springframework.messaging.simp.SimpMessageSendingOperations", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.simp.SimpMessagingTemplate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "convertAndSendToUser", + "parameterTypes": [ + "java.lang.String", + "java.lang.String", + "java.lang.Object" + ] + }, + { + "name": "convertAndSendToUser", + "parameterTypes": [ + "java.lang.String", + "java.lang.String", + "java.lang.Object", + "java.util.Map" + ] + }, + { + "name": "convertAndSendToUser", + "parameterTypes": [ + "java.lang.String", + "java.lang.String", + "java.lang.Object", + "java.util.Map", + "org.springframework.messaging.core.MessagePostProcessor" + ] + }, + { + "name": "convertAndSendToUser", + "parameterTypes": [ + "java.lang.String", + "java.lang.String", + "java.lang.Object", + "org.springframework.messaging.core.MessagePostProcessor" + ] + }, + { + "name": "send", + "parameterTypes": [ + "org.springframework.messaging.Message" + ] + } + ] +}, +{ + "name":"org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getConversionService", + "parameterTypes": [] + }, + { + "name": "getHeaderInitializer", + "parameterTypes": [] + }, + { + "name": "getMessageConverter", + "parameterTypes": [] + }, + { + "name": "getPathMatcher", + "parameterTypes": [] + }, + { + "name": "getValidator", + "parameterTypes": [] + }, + { + "name": "isRunning", + "parameterTypes": [] + }, + { + "name": "setConversionService", + "parameterTypes": [ + "org.springframework.core.convert.ConversionService" + ] + }, + { + "name": "setDestinationPrefixes", + "parameterTypes": [ + "java.util.Collection" + ] + }, + { + "name": "setEmbeddedValueResolver", + "parameterTypes": [ + "org.springframework.util.StringValueResolver" + ] + }, + { + "name": "setHeaderInitializer", + "parameterTypes": [ + "org.springframework.messaging.support.MessageHeaderInitializer" + ] + }, + { + "name": "setMessageConverter", + "parameterTypes": [ + "org.springframework.messaging.converter.MessageConverter" + ] + }, + { + "name": "setPathMatcher", + "parameterTypes": [ + "org.springframework.util.PathMatcher" + ] + }, + { + "name": "setValidator", + "parameterTypes": [ + "org.springframework.validation.Validator" + ] + }, + { + "name": "start", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [ + "java.lang.Runnable" + ] + } + ] +}, +{ + "name":"org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getApplicationEventPublisher", + "parameterTypes": [] + }, + { + "name": "getBrokerChannel", + "parameterTypes": [] + }, + { + "name": "getClientInboundChannel", + "parameterTypes": [] + }, + { + "name": "getClientOutboundChannel", + "parameterTypes": [] + }, + { + "name": "getDestinationPrefixes", + "parameterTypes": [] + }, + { + "name": "handleMessage", + "parameterTypes": [ + "org.springframework.messaging.Message" + ] + }, + { + "name": "isAutoStartup", + "parameterTypes": [] + }, + { + "name": "isBrokerAvailable", + "parameterTypes": [] + }, + { + "name": "isPreservePublishOrder", + "parameterTypes": [] + }, + { + "name": "isRunning", + "parameterTypes": [] + }, + { + "name": "setApplicationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + }, + { + "name": "setAutoStartup", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setPreservePublishOrder", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUserDestinationPredicate", + "parameterTypes": [ + "java.util.function.Predicate" + ] + }, + { + "name": "start", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [ + "java.lang.Runnable" + ] + } + ] +}, +{ + "name":"org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"toString","parameterTypes":[] }] +}, +{ + "name":"org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "brokerChannel", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.core.task.TaskExecutor" + ] + }, + { + "name": "brokerChannelExecutor", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel" + ] + }, + { + "name": "brokerMessageConverter", + "parameterTypes": [] + }, + { + "name": "brokerMessagingTemplate", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.converter.CompositeMessageConverter" + ] + }, + { + "name": "clientInboundChannel", + "parameterTypes": [ + "org.springframework.core.task.TaskExecutor" + ] + }, + { + "name": "clientInboundChannelExecutor", + "parameterTypes": [] + }, + { + "name": "clientOutboundChannel", + "parameterTypes": [ + "org.springframework.core.task.TaskExecutor" + ] + }, + { + "name": "clientOutboundChannelExecutor", + "parameterTypes": [] + }, + { + "name": "getApplicationContext", + "parameterTypes": [] + }, + { + "name": "getPathMatcher", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel" + ] + }, + { + "name": "getValidator", + "parameterTypes": [] + }, + { + "name": "messageBrokerTaskScheduler", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "simpAnnotationMethodMessageHandler", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.simp.SimpMessagingTemplate", + "org.springframework.messaging.converter.CompositeMessageConverter" + ] + }, + { + "name": "simpleBrokerMessageHandler", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.simp.user.UserDestinationResolver" + ] + }, + { + "name": "stompBrokerRelayMessageHandler", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.simp.user.UserDestinationMessageHandler", + "org.springframework.messaging.MessageHandler", + "org.springframework.messaging.simp.user.UserDestinationResolver" + ] + }, + { + "name": "userDestinationMessageHandler", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.simp.user.UserDestinationResolver" + ] + }, + { + "name": "userDestinationResolver", + "parameterTypes": [ + "org.springframework.messaging.simp.user.SimpUserRegistry", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel" + ] + }, + { + "name": "userRegistry", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel" + ] + }, + { + "name": "userRegistryMessageHandler", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.simp.user.SimpUserRegistry", + "org.springframework.messaging.simp.SimpMessagingTemplate", + "org.springframework.scheduling.TaskScheduler" + ] + } + ] +}, +{ + "name":"org.springframework.messaging.simp.user.DefaultUserDestinationResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "resolveDestination", + "parameterTypes": [ + "org.springframework.messaging.Message" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.messaging.simp.user.SimpUserRegistry", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.simp.user.UserDestinationMessageHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "handleMessage", + "parameterTypes": [ + "org.springframework.messaging.Message" + ] + }, + { + "name": "isRunning", + "parameterTypes": [] + }, + { + "name": "start", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.messaging.simp.user.UserDestinationResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.messaging.support.AbstractMessageChannel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getBeanName", + "parameterTypes": [] + }, + { + "name": "getInterceptors", + "parameterTypes": [] + }, + { + "name": "getLogger", + "parameterTypes": [] + }, + { + "name": "removeInterceptor", + "parameterTypes": [ + "int" + ] + }, + { + "name": "removeInterceptor", + "parameterTypes": [ + "org.springframework.messaging.support.ChannelInterceptor" + ] + }, + { + "name": "send", + "parameterTypes": [ + "org.springframework.messaging.Message" + ] + }, + { + "name": "send", + "parameterTypes": [ + "org.springframework.messaging.Message", + "long" + ] + }, + { + "name": "setBeanName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setLogger", + "parameterTypes": [ + "org.apache.commons.logging.Log" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.messaging.support.AbstractSubscribableChannel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getSubscribers", + "parameterTypes": [] + }, + { + "name": "hasSubscription", + "parameterTypes": [ + "org.springframework.messaging.MessageHandler" + ] + }, + { + "name": "subscribe", + "parameterTypes": [ + "org.springframework.messaging.MessageHandler" + ] + }, + { + "name": "unsubscribe", + "parameterTypes": [ + "org.springframework.messaging.MessageHandler" + ] + } + ] +}, +{ + "name":"org.springframework.messaging.support.ExecutorSubscribableChannel", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addInterceptor", + "parameterTypes": [ + "int", + "org.springframework.messaging.support.ChannelInterceptor" + ] + }, + { + "name": "addInterceptor", + "parameterTypes": [ + "org.springframework.messaging.support.ChannelInterceptor" + ] + }, + { + "name": "setInterceptors", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.springframework.messaging.support.InterceptableChannel", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.orm.hibernate5.SpringSessionContext", + "methods":[{"name":"","parameterTypes":["org.hibernate.engine.spi.SessionFactoryImplementor"] }] +}, +{ + "name":"org.springframework.orm.jpa.AbstractEntityManagerFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "createNativeEntityManager", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "getBeanClassLoader", + "parameterTypes": [] + }, + { + "name": "getBootstrapExecutor", + "parameterTypes": [] + }, + { + "name": "getEntityManagerInterface", + "parameterTypes": [] + }, + { + "name": "getJpaDialect", + "parameterTypes": [] + }, + { + "name": "getJpaPropertyMap", + "parameterTypes": [] + }, + { + "name": "getJpaVendorAdapter", + "parameterTypes": [] + }, + { + "name": "getNativeEntityManagerFactory", + "parameterTypes": [] + }, + { + "name": "getObject", + "parameterTypes": [] + }, + { + "name": "getObjectType", + "parameterTypes": [] + }, + { + "name": "getPersistenceProvider", + "parameterTypes": [] + }, + { + "name": "isSingleton", + "parameterTypes": [] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "setBeanName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setBootstrapExecutor", + "parameterTypes": [ + "org.springframework.core.task.AsyncTaskExecutor" + ] + }, + { + "name": "setEntityManagerFactoryInterface", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setEntityManagerInitializer", + "parameterTypes": [ + "java.util.function.Consumer" + ] + }, + { + "name": "setEntityManagerInterface", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setJpaDialect", + "parameterTypes": [ + "org.springframework.orm.jpa.JpaDialect" + ] + }, + { + "name": "setJpaProperties", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setJpaPropertyMap", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setJpaVendorAdapter", + "parameterTypes": [ + "org.springframework.orm.jpa.JpaVendorAdapter" + ] + }, + { + "name": "setPersistenceProvider", + "parameterTypes": [ + "jakarta.persistence.spi.PersistenceProvider" + ] + }, + { + "name": "setPersistenceProviderClass", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "translateExceptionIfPossible", + "parameterTypes": [ + "java.lang.RuntimeException" + ] + } + ] +}, +{ + "name":"org.springframework.orm.jpa.EntityManagerFactoryInfo", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "createNativeEntityManager", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "getBeanClassLoader", + "parameterTypes": [] + }, + { + "name": "getDataSource", + "parameterTypes": [] + }, + { + "name": "getEntityManagerInterface", + "parameterTypes": [] + }, + { + "name": "getJpaDialect", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.orm.jpa.EntityManagerProxy", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.orm.jpa.JpaTransactionManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getResourceFactory", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + } + ] +}, +{ + "name":"org.springframework.orm.jpa.JpaVendorAdapter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getDataSource", + "parameterTypes": [] + }, + { + "name": "getPersistenceUnitInfo", + "parameterTypes": [] + }, + { + "name": "getPersistenceUnitName", + "parameterTypes": [] + }, + { + "name": "setLoadTimeWeaver", + "parameterTypes": [ + "org.springframework.instrument.classloading.LoadTimeWeaver" + ] + }, + { + "name": "setPersistenceUnitName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setResourceLoader", + "parameterTypes": [ + "org.springframework.core.io.ResourceLoader" + ] + } + ] +}, +{ + "name":"org.springframework.orm.jpa.SharedEntityManagerCreator", + "queryAllDeclaredMethods":true, + "methods":[{"name":"createSharedEntityManager","parameterTypes":["jakarta.persistence.EntityManagerFactory"] }] +}, +{ + "name":"org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.orm.jpa.persistenceunit.SimplePersistenceManagedTypes", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getManagedClassNames", + "parameterTypes": [] + }, + { + "name": "getManagedPackages", + "parameterTypes": [] + }, + { + "name": "getPersistenceUnitRootUrl", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "postProcessEntityManager", + "parameterTypes": [ + "jakarta.persistence.EntityManager" + ] + }, + { + "name": "postProcessEntityManagerFactory", + "parameterTypes": [ + "jakarta.persistence.EntityManagerFactory" + ] + }, + { + "name": "setDatabase", + "parameterTypes": [ + "org.springframework.orm.jpa.vendor.Database" + ] + }, + { + "name": "setDatabasePlatform", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setGenerateDdl", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setShowSql", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getEntityManagerFactoryInterface", + "parameterTypes": [] + }, + { + "name": "getEntityManagerInterface", + "parameterTypes": [] + }, + { + "name": "getJpaDialect", + "parameterTypes": [] + }, + { + "name": "getJpaPropertyMap", + "parameterTypes": [] + }, + { + "name": "getJpaPropertyMap", + "parameterTypes": [ + "jakarta.persistence.spi.PersistenceUnitInfo" + ] + }, + { + "name": "getPersistenceProvider", + "parameterTypes": [] + }, + { + "name": "getPersistenceProviderRootPackage", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.oxm.GenericMarshaller", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.oxm.GenericUnmarshaller", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.oxm.Marshaller", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.oxm.Unmarshaller", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.oxm.jaxb.Jaxb2Marshaller", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "marshal", + "parameterTypes": [ + "java.lang.Object", + "javax.xml.transform.Result" + ] + }, + { + "name": "marshal", + "parameterTypes": [ + "java.lang.Object", + "javax.xml.transform.Result", + "org.springframework.oxm.mime.MimeContainer" + ] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.reflect.Type" + ] + }, + { + "name": "unmarshal", + "parameterTypes": [ + "javax.xml.transform.Source" + ] + }, + { + "name": "unmarshal", + "parameterTypes": [ + "javax.xml.transform.Source", + "org.springframework.oxm.mime.MimeContainer" + ] + } + ] +}, +{ + "name":"org.springframework.oxm.mime.MimeMarshaller", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.oxm.mime.MimeUnmarshaller", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.r2dbc.connection.R2dbcTransactionManager" +}, +{ + "name":"org.springframework.r2dbc.connection.init.DatabasePopulator" +}, +{ + "name":"org.springframework.scheduling.SchedulingTaskExecutor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"prefersShortLivedTasks","parameterTypes":[] }] +}, +{ + "name":"org.springframework.scheduling.TaskScheduler", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "schedule", + "parameterTypes": [ + "java.lang.Runnable", + "java.util.Date" + ] + }, + { + "name": "scheduleAtFixedRate", + "parameterTypes": [ + "java.lang.Runnable", + "long" + ] + }, + { + "name": "scheduleAtFixedRate", + "parameterTypes": [ + "java.lang.Runnable", + "java.util.Date", + "long" + ] + }, + { + "name": "scheduleWithFixedDelay", + "parameterTypes": [ + "java.lang.Runnable", + "long" + ] + }, + { + "name": "scheduleWithFixedDelay", + "parameterTypes": [ + "java.lang.Runnable", + "java.util.Date", + "long" + ] + } + ] +}, +{ + "name":"org.springframework.scheduling.annotation.AbstractAsyncConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "setConfigurers", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + }, + { + "name": "setImportMetadata", + "parameterTypes": [ + "org.springframework.core.type.AnnotationMetadata" + ] + } + ] +}, +{ + "name":"org.springframework.scheduling.annotation.Async", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setBeanFactory","parameterTypes":["org.springframework.beans.factory.BeanFactory"] }] +}, +{ + "name":"org.springframework.scheduling.annotation.AsyncConfigurationSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.scheduling.annotation.EnableAsync", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.scheduling.annotation.EnableScheduling", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.scheduling.annotation.ProxyAsyncConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "asyncAdvisor", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "postProcessAfterInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "postProcessBeforeInitialization", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String" + ] + }, + { + "name": "requiresDestruction", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.scheduling.annotation.Schedules", + "queryAllDeclaredMethods":true +}, + { + "name": "org.springframework.scheduling.annotation.SchedulingConfiguration", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "scheduledAnnotationProcessor", + "parameterTypes": [] + } + ] + }, +{ + "name":"org.springframework.scheduling.annotation.SchedulingConfigurer" +}, +{ + "name":"org.springframework.scheduling.concurrent.CustomizableThreadFactory", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"newThread","parameterTypes":["java.lang.Runnable"] }] +}, +{ + "name":"org.springframework.scheduling.concurrent.ExecutorConfigurationSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "initialize", + "parameterTypes": [] + }, + { + "name": "setAwaitTerminationMillis", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setAwaitTerminationSeconds", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setBeanName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setRejectedExecutionHandler", + "parameterTypes": [ + "java.util.concurrent.RejectedExecutionHandler" + ] + }, + { + "name": "setThreadFactory", + "parameterTypes": [ + "java.util.concurrent.ThreadFactory" + ] + }, + { + "name": "setThreadNamePrefix", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setWaitForTasksToCompleteOnShutdown", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "shutdown", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "execute", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.Runnable", + "long" + ] + }, + { + "name": "submit", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "submit", + "parameterTypes": [ + "java.util.concurrent.Callable" + ] + }, + { + "name": "submitListenable", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "submitListenable", + "parameterTypes": [ + "java.util.concurrent.Callable" + ] + } + ] +}, +{ + "name":"org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "execute", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.Runnable", + "long" + ] + }, + { + "name": "getClock", + "parameterTypes": [] + }, + { + "name": "schedule", + "parameterTypes": [ + "java.lang.Runnable", + "java.time.Instant" + ] + }, + { + "name": "schedule", + "parameterTypes": [ + "java.lang.Runnable", + "org.springframework.scheduling.Trigger" + ] + }, + { + "name": "scheduleAtFixedRate", + "parameterTypes": [ + "java.lang.Runnable", + "java.time.Duration" + ] + }, + { + "name": "scheduleAtFixedRate", + "parameterTypes": [ + "java.lang.Runnable", + "java.time.Instant", + "java.time.Duration" + ] + }, + { + "name": "scheduleWithFixedDelay", + "parameterTypes": [ + "java.lang.Runnable", + "java.time.Duration" + ] + }, + { + "name": "scheduleWithFixedDelay", + "parameterTypes": [ + "java.lang.Runnable", + "java.time.Instant", + "java.time.Duration" + ] + }, + { + "name": "submit", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "submit", + "parameterTypes": [ + "java.util.concurrent.Callable" + ] + }, + { + "name": "submitListenable", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "submitListenable", + "parameterTypes": [ + "java.util.concurrent.Callable" + ] + } + ] +}, +{ + "name":"org.springframework.scheduling.config.ScheduledTaskHolder", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.access.annotation.Secured", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.security.access.expression.AbstractSecurityExpressionHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "createEvaluationContext", + "parameterTypes": [ + "org.springframework.security.core.Authentication", + "java.lang.Object" + ] + }, + { + "name": "createEvaluationContextInternal", + "parameterTypes": [ + "org.springframework.security.core.Authentication", + "java.lang.Object" + ] + }, + { + "name": "createSecurityExpressionRoot", + "parameterTypes": [ + "org.springframework.security.core.Authentication", + "java.lang.Object" + ] + }, + { + "name": "getExpressionParser", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setExpressionParser", + "parameterTypes": [ + "org.springframework.expression.ExpressionParser" + ] + }, + { + "name": "setPermissionEvaluator", + "parameterTypes": [ + "org.springframework.security.access.PermissionEvaluator" + ] + }, + { + "name": "setRoleHierarchy", + "parameterTypes": [ + "org.springframework.security.access.hierarchicalroles.RoleHierarchy" + ] + } + ] +}, +{ + "name":"org.springframework.security.access.expression.SecurityExpressionHandler", + "queryAllPublicMethods":true, + "methods":[{"name":"createEvaluationContext","parameterTypes":["java.util.function.Supplier","java.lang.Object"] }] +}, +{ + "name":"org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"createEvaluationContext","parameterTypes":["java.util.function.Supplier","java.lang.Object"] }] +}, +{ + "name":"org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.access.expression.method.MethodSecurityExpressionHandler", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.access.intercept.AbstractSecurityInterceptor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getAccessDecisionManager", + "parameterTypes": [] + }, + { + "name": "getAfterInvocationManager", + "parameterTypes": [] + }, + { + "name": "getAuthenticationManager", + "parameterTypes": [] + }, + { + "name": "getRunAsManager", + "parameterTypes": [] + }, + { + "name": "isAlwaysReauthenticate", + "parameterTypes": [] + }, + { + "name": "isRejectPublicInvocations", + "parameterTypes": [] + }, + { + "name": "isValidateConfigAttributes", + "parameterTypes": [] + }, + { + "name": "setAccessDecisionManager", + "parameterTypes": [ + "org.springframework.security.access.AccessDecisionManager" + ] + }, + { + "name": "setAfterInvocationManager", + "parameterTypes": [ + "org.springframework.security.access.intercept.AfterInvocationManager" + ] + }, + { + "name": "setAlwaysReauthenticate", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setApplicationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + }, + { + "name": "setAuthenticationManager", + "parameterTypes": [ + "org.springframework.security.authentication.AuthenticationManager" + ] + }, + { + "name": "setMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + }, + { + "name": "setPublishAuthorizationSuccess", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRejectPublicInvocations", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRunAsManager", + "parameterTypes": [ + "org.springframework.security.access.intercept.RunAsManager" + ] + }, + { + "name": "setSecurityContextHolderStrategy", + "parameterTypes": [ + "org.springframework.security.core.context.SecurityContextHolderStrategy" + ] + }, + { + "name": "setValidateConfigAttributes", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getSecureObjectClass", + "parameterTypes": [] + }, + { + "name": "invoke", + "parameterTypes": [ + "org.aopalliance.intercept.MethodInvocation" + ] + }, + { + "name": "obtainSecurityMetadataSource", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.String", + "org.springframework.security.access.method.MethodSecurityMetadataSource", + "java.lang.String" + ] + }, + { + "name": "getAdvice", + "parameterTypes": [] + }, + { + "name": "getPointcut", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + } + ] +}, +{ + "name":"org.springframework.security.access.method.AbstractMethodSecurityMetadataSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getAttributes", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.security.access.method.MethodSecurityMetadataSource", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.access.prepost.PreInvocationAuthorizationAdvice", + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.authentication.AnonymousAuthenticationProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "authenticate", + "parameterTypes": [ + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "setMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.security.authentication.AuthenticationEventPublisher", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.authentication.AuthenticationManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.authentication.AuthenticationManagerResolver" +}, +{ + "name":"org.springframework.security.authentication.AuthenticationProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.authentication.DefaultAuthenticationEventPublisher", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "publishAuthenticationFailure", + "parameterTypes": [ + "org.springframework.security.core.AuthenticationException", + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "publishAuthenticationSuccess", + "parameterTypes": [ + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "setApplicationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + } + ] +}, +{ + "name":"org.springframework.security.authentication.ProviderManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "authenticate", + "parameterTypes": [ + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "setMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + } + ] +}, +{ + "name":"org.springframework.security.authentication.ReactiveAuthenticationManager" +}, +{ + "name":"org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "authenticate", + "parameterTypes": [ + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "getUserCache", + "parameterTypes": [] + }, + { + "name": "isForcePrincipalAsString", + "parameterTypes": [] + }, + { + "name": "isHideUserNotFoundExceptions", + "parameterTypes": [] + }, + { + "name": "setAuthoritiesMapper", + "parameterTypes": [ + "org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper" + ] + }, + { + "name": "setForcePrincipalAsString", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setHideUserNotFoundExceptions", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + }, + { + "name": "setPostAuthenticationChecks", + "parameterTypes": [ + "org.springframework.security.core.userdetails.UserDetailsChecker" + ] + }, + { + "name": "setPreAuthenticationChecks", + "parameterTypes": [ + "org.springframework.security.core.userdetails.UserDetailsChecker" + ] + }, + { + "name": "setUserCache", + "parameterTypes": [ + "org.springframework.security.core.userdetails.UserCache" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.security.authentication.dao.DaoAuthenticationProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureCredentialsExpiredEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureDisabledEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureExpiredEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureLockedEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureProviderNotFoundEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureProxyUntrustedEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authentication.event.AuthenticationFailureServiceExceptionEvent", + "methods":[{"name":"","parameterTypes":["org.springframework.security.core.Authentication","org.springframework.security.core.AuthenticationException"] }] +}, +{ + "name":"org.springframework.security.authorization.AuthorizationManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"verify","parameterTypes":["java.util.function.Supplier","java.lang.Object"] }] +}, +{ + "name":"org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "apply", + "parameterTypes": [ + "org.springframework.security.config.annotation.SecurityConfigurer" + ] + }, + { + "name": "apply", + "parameterTypes": [ + "org.springframework.security.config.annotation.SecurityConfigurerAdapter" + ] + }, + { + "name": "getConfigurer", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getConfigurers", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getOrBuild", + "parameterTypes": [] + }, + { + "name": "getSharedObject", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "getSharedObjects", + "parameterTypes": [] + }, + { + "name": "objectPostProcessor", + "parameterTypes": [ + "org.springframework.security.config.annotation.ObjectPostProcessor" + ] + }, + { + "name": "removeConfigurer", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "removeConfigurers", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setSharedObject", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.AbstractSecurityBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "build", + "parameterTypes": [] + }, + { + "name": "getObject", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.ObjectPostProcessor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.config.annotation.SecurityBuilder", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.config.annotation.SecurityConfigurer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"init","parameterTypes":["org.springframework.security.config.annotation.SecurityBuilder"] }] +}, +{ + "name":"org.springframework.security.config.annotation.authentication.ProviderManagerBuilder", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "authenticationEventPublisher", + "parameterTypes": [ + "org.springframework.security.authentication.AuthenticationEventPublisher" + ] + }, + { + "name": "authenticationProvider", + "parameterTypes": [ + "org.springframework.security.authentication.AuthenticationProvider" + ] + }, + { + "name": "eraseCredentials", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "getDefaultUserDetailsService", + "parameterTypes": [] + }, + { + "name": "isConfigured", + "parameterTypes": [] + }, + { + "name": "ldapAuthentication", + "parameterTypes": [] + }, + { + "name": "parentAuthenticationManager", + "parameterTypes": [ + "org.springframework.security.authentication.AuthenticationManager" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "authenticationManagerBuilder", + "parameterTypes": [ + "org.springframework.security.config.annotation.ObjectPostProcessor", + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "enableGlobalAuthenticationAutowiredConfigurer", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "initializeAuthenticationProviderBeanManagerConfigurer", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "initializeUserDetailsBeanManagerConfigurer", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setGlobalAuthenticationConfigurers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setObjectPostProcessor", + "parameterTypes": [ + "org.springframework.security.config.annotation.ObjectPostProcessor" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "inMemoryAuthentication", + "parameterTypes": [] + }, + { + "name": "jdbcAuthentication", + "parameterTypes": [] + }, + { + "name": "userDetailsService", + "parameterTypes": [ + "org.springframework.security.core.userdetails.UserDetailsService" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "init", + "parameterTypes": [ + "org.springframework.security.config.annotation.SecurityBuilder" + ] + }, + { + "name": "init", + "parameterTypes": [ + "org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "configure", + "parameterTypes": [ + "org.springframework.security.config.annotation.SecurityBuilder" + ] + }, + { + "name": "configure", + "parameterTypes": [ + "org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder" + ] + }, + { + "name": "init", + "parameterTypes": [ + "org.springframework.security.config.annotation.SecurityBuilder" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.authentication.configuration.InitializeAuthenticationProviderBeanManagerConfigurer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "init", + "parameterTypes": [ + "org.springframework.security.config.annotation.SecurityBuilder" + ] + }, + { + "name": "init", + "parameterTypes": [ + "org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "init", + "parameterTypes": [ + "org.springframework.security.config.annotation.SecurityBuilder" + ] + }, + { + "name": "init", + "parameterTypes": [ + "org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterSingletonsInstantiated", + "parameterTypes": [] + }, + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "postProcess", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "objectPostProcessor", + "parameterTypes": [ + "org.springframework.beans.factory.config.AutowireCapableBeanFactory" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterSingletonsInstantiated", + "parameterTypes": [] + }, + { + "name": "methodSecurityInterceptor", + "parameterTypes": [ + "org.springframework.security.access.method.MethodSecurityMetadataSource" + ] + }, + { + "name": "preInvocationAuthorizationAdvice", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "setImportMetadata", + "parameterTypes": [ + "org.springframework.core.type.AnnotationMetadata" + ] + }, + { + "name": "setMethodSecurityExpressionHandler", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setObjectPostProcessor", + "parameterTypes": [ + "org.springframework.security.config.annotation.ObjectPostProcessor" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration$$SpringCGLIB$$0", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.springframework.security.config.annotation.method.configuration.GlobalMethodSecuritySelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.security.config.annotation.method.configuration.MethodSecurityMetadataSourceAdvisorRegistrar", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.security.config.annotation.web.HttpSecurityBuilder", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.config.annotation.web.builders.HttpSecurity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addFilter", + "parameterTypes": [ + "jakarta.servlet.Filter" + ] + }, + { + "name": "addFilterAfter", + "parameterTypes": [ + "jakarta.servlet.Filter", + "java.lang.Class" + ] + }, + { + "name": "addFilterBefore", + "parameterTypes": [ + "jakarta.servlet.Filter", + "java.lang.Class" + ] + }, + { + "name": "authenticationProvider", + "parameterTypes": [ + "org.springframework.security.authentication.AuthenticationProvider" + ] + }, + { + "name": "setSharedObject", + "parameterTypes": [ + "java.lang.Class", + "java.lang.Object" + ] + }, + { + "name": "userDetailsService", + "parameterTypes": [ + "org.springframework.security.core.userdetails.UserDetailsService" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.web.builders.WebSecurity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setServletContext", + "parameterTypes": [ + "jakarta.servlet.ServletContext" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.web.configuration.EnableWebSecurity", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "httpSecurity", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setAuthenticationConfiguration", + "parameterTypes": [ + "org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration" + ] + }, + { + "name": "setContentNegotiationStrategy", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationStrategy" + ] + }, + { + "name": "setObjectPostProcessor", + "parameterTypes": [ + "org.springframework.security.config.annotation.ObjectPostProcessor" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.web.configuration.OAuth2ImportSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.security.config.annotation.web.configuration.SpringWebMvcImportSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "addArgumentResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "requestDataValueProcessor", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "conversionServicePostProcessor", + "parameterTypes": [] + }, + { + "name": "delegatingApplicationListener", + "parameterTypes": [] + }, + { + "name": "privilegeEvaluator", + "parameterTypes": [] + }, + { + "name": "setBeanClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setFilterChainProxySecurityConfigurer", + "parameterTypes": [ + "org.springframework.security.config.annotation.ObjectPostProcessor", + "org.springframework.beans.factory.config.ConfigurableListableBeanFactory" + ] + }, + { + "name": "setFilterChains", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "setImportMetadata", + "parameterTypes": [ + "org.springframework.core.type.AnnotationMetadata" + ] + }, + { + "name": "springSecurityFilterChain", + "parameterTypes": [] + }, + { + "name": "webSecurityExpressionHandler", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity" +}, +{ + "name":"org.springframework.security.config.http.SessionCreationPolicy" +}, +{ + "name":"org.springframework.security.context.DelegatingApplicationListener", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"onApplicationEvent","parameterTypes":["org.springframework.context.ApplicationEvent"] }] +}, +{ + "name":"org.springframework.security.core.userdetails.UserDetailsService", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.data.repository.query.SecurityEvaluationContextExtension" +}, +{ + "name":"org.springframework.security.oauth2.client.registration$ClientRegistration" +}, +{ + "name":"org.springframework.security.oauth2.client.registration.ClientRegistration" +}, +{ + "name":"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" +}, +{ + "name":"org.springframework.security.oauth2.jwt.JwtDecoder" +}, +{ + "name":"org.springframework.security.oauth2.server.resource$BearerTokenError" +}, +{ + "name":"org.springframework.security.oauth2.server.resource.BearerTokenError" +}, +{ + "name":"org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken" +}, +{ + "name":"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" +}, +{ + "name":"org.springframework.security.provisioning.UserDetailsManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor" +}, +{ + "name":"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" +}, +{ + "name":"org.springframework.security.web.DefaultSecurityFilterChain", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getFilters", + "parameterTypes": [] + }, + { + "name": "matches", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.security.web.FilterChainProxy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "doFilter", + "parameterTypes": [ + "jakarta.servlet.ServletRequest", + "jakarta.servlet.ServletResponse", + "jakarta.servlet.FilterChain" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.security.web.SecurityFilterChain", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.access.AccessDeniedHandler", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.access.AccessDeniedHandlerImpl", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setErrorPage","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"org.springframework.security.web.access.ExceptionTranslationFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "doFilter", + "parameterTypes": [ + "jakarta.servlet.ServletRequest", + "jakarta.servlet.ServletResponse", + "jakarta.servlet.FilterChain" + ] + }, + { + "name": "setMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + } + ] +}, +{ + "name":"org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "isAllowed", + "parameterTypes": [ + "java.lang.String", + "java.lang.String", + "java.lang.String", + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "isAllowed", + "parameterTypes": [ + "java.lang.String", + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "setServletContext", + "parameterTypes": [ + "jakarta.servlet.ServletContext" + ] + } + ] +}, +{ + "name":"org.springframework.security.web.access.WebInvocationPrivilegeEvaluator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.access.intercept.AuthorizationFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"doFilter","parameterTypes":["jakarta.servlet.ServletRequest","jakarta.servlet.ServletResponse","jakarta.servlet.FilterChain"] }] +}, +{ + "name":"org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"check","parameterTypes":["java.util.function.Supplier","java.lang.Object"] }] +}, +{ + "name":"org.springframework.security.web.authentication.AnonymousAuthenticationFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setSecurityContextHolderStrategy","parameterTypes":["org.springframework.security.core.context.SecurityContextHolderStrategy"] }] +}, +{ + "name":"org.springframework.security.web.authentication.logout.LogoutFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"doFilter","parameterTypes":["jakarta.servlet.ServletRequest","jakarta.servlet.ServletResponse","jakarta.servlet.FilterChain"] }] +}, +{ + "name":"org.springframework.security.web.authentication.logout.LogoutHandler", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "logout", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "org.springframework.security.core.Authentication" + ] + }, + { + "name": "setApplicationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + } + ] +}, +{ + "name":"org.springframework.security.web.authentication.session.AbstractSessionFixationProtectionStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "onAuthentication", + "parameterTypes": [ + "org.springframework.security.core.Authentication", + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse" + ] + }, + { + "name": "setAlwaysCreateSession", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setApplicationEventPublisher", + "parameterTypes": [ + "org.springframework.context.ApplicationEventPublisher" + ] + } + ] +}, +{ + "name":"org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "onAuthentication", + "parameterTypes": [ + "org.springframework.security.core.Authentication", + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.security.web.authentication.session.SessionAuthenticationStrategy", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer" +}, + { + "name": "org.springframework.security.web.context.SecurityContextHolderFilter", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "methods": [ + { + "name": "doFilter", + "parameterTypes": [ + "jakarta.servlet.ServletRequest", + "jakarta.servlet.ServletResponse", + "jakarta.servlet.FilterChain" + ] + } + ] + }, +{ + "name":"org.springframework.security.web.firewall.DefaultHttpFirewall", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getFirewalledRequest", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "getFirewalledResponse", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletResponse" + ] + } + ] +}, +{ + "name":"org.springframework.security.web.firewall.HttpFirewall", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.header.HeaderWriterFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.security.web.savedrequest.RequestCacheAwareFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"doFilter","parameterTypes":["jakarta.servlet.ServletRequest","jakarta.servlet.ServletResponse","jakarta.servlet.FilterChain"] }] +}, +{ + "name":"org.springframework.security.web.server.WebFilterChainProxy" +}, +{ + "name":"org.springframework.security.web.server.csrf.CsrfToken" +}, +{ + "name":"org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getExtraHiddenFields", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "processAction", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "processFormFieldValue", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.String", + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "processUrl", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "doFilter", + "parameterTypes": [ + "jakarta.servlet.ServletRequest", + "jakarta.servlet.ServletResponse", + "jakarta.servlet.FilterChain" + ] + } + ] +}, +{ + "name":"org.springframework.security.web.util.matcher.RequestMatcher" +}, +{ + "name":"org.springframework.session.FindByIndexNameSessionRepository" +}, +{ + "name":"org.springframework.session.Session" +}, +{ + "name":"org.springframework.stereotype.Component", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.stereotype.Controller", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.stereotype.Indexed", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.stereotype.Repository", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.transaction.PlatformTransactionManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.transaction.ReactiveTransactionManager" +}, +{ + "name":"org.springframework.transaction.TransactionDefinition", + "allPublicFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.transaction.TransactionManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "setImportMetadata", + "parameterTypes": [ + "org.springframework.core.type.AnnotationMetadata" + ] + }, + { + "name": "transactionalEventListenerFactory", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.transaction.annotation.AnnotationTransactionAttributeSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isCandidateClass", + "parameterTypes": [ + "java.lang.Class" + ] + } + ] +}, +{ + "name":"org.springframework.transaction.annotation.EnableTransactionManagement", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "transactionAdvisor", + "parameterTypes": [ + "org.springframework.transaction.interceptor.TransactionAttributeSource", + "org.springframework.transaction.interceptor.TransactionInterceptor" + ] + }, + { + "name": "transactionAttributeSource", + "parameterTypes": [] + }, + { + "name": "transactionInterceptor", + "parameterTypes": [ + "org.springframework.transaction.interceptor.TransactionAttributeSource" + ] + } + ] +}, +{ + "name":"org.springframework.transaction.annotation.TransactionManagementConfigurationSelector", + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.springframework.transaction.annotation.TransactionRuntimeHints" +}, +{ + "name":"org.springframework.transaction.annotation.Transactional", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getTransactionAttribute", + "parameterTypes": [ + "java.lang.reflect.Method", + "java.lang.Class" + ] + }, + { + "name": "setEmbeddedValueResolver", + "parameterTypes": [ + "org.springframework.util.StringValueResolver" + ] + } + ] +}, +{ + "name":"org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getPointcut","parameterTypes":[] }] +}, +{ + "name":"org.springframework.transaction.interceptor.TransactionAspectSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "currentTransactionStatus", + "parameterTypes": [] + }, + { + "name": "getTransactionAttributeSource", + "parameterTypes": [] + }, + { + "name": "getTransactionManager", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + }, + { + "name": "setTransactionAttributeSource", + "parameterTypes": [ + "org.springframework.transaction.interceptor.TransactionAttributeSource" + ] + }, + { + "name": "setTransactionAttributeSources", + "parameterTypes": [ + "org.springframework.transaction.interceptor.TransactionAttributeSource[]" + ] + }, + { + "name": "setTransactionAttributes", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setTransactionManager", + "parameterTypes": [ + "org.springframework.transaction.TransactionManager" + ] + }, + { + "name": "setTransactionManagerBeanName", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.transaction.interceptor.TransactionAttributeSource", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.transaction.interceptor.TransactionInterceptor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"invoke","parameterTypes":["org.aopalliance.intercept.MethodInvocation"] }] +}, +{ + "name":"org.springframework.transaction.interceptor.TransactionalProxy", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.transaction.jta.JtaTransactionManager" +}, +{ + "name":"org.springframework.transaction.support.AbstractPlatformTransactionManager", + "allDeclaredFields":true, + "allPublicFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "commit", + "parameterTypes": [ + "org.springframework.transaction.TransactionStatus" + ] + }, + { + "name": "getDefaultTimeout", + "parameterTypes": [] + }, + { + "name": "getTransaction", + "parameterTypes": [ + "org.springframework.transaction.TransactionDefinition" + ] + }, + { + "name": "getTransactionSynchronization", + "parameterTypes": [] + }, + { + "name": "isFailEarlyOnGlobalRollbackOnly", + "parameterTypes": [] + }, + { + "name": "isGlobalRollbackOnParticipationFailure", + "parameterTypes": [] + }, + { + "name": "isNestedTransactionAllowed", + "parameterTypes": [] + }, + { + "name": "isRollbackOnCommitFailure", + "parameterTypes": [] + }, + { + "name": "isValidateExistingTransaction", + "parameterTypes": [] + }, + { + "name": "rollback", + "parameterTypes": [ + "org.springframework.transaction.TransactionStatus" + ] + }, + { + "name": "setDefaultTimeout", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setFailEarlyOnGlobalRollbackOnly", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setGlobalRollbackOnParticipationFailure", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setNestedTransactionAllowed", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRollbackOnCommitFailure", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setTransactionSynchronization", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setTransactionSynchronizationName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setValidateExistingTransaction", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.transaction.support.DefaultTransactionDefinition", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getIsolationLevel", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getPropagationBehavior", + "parameterTypes": [] + }, + { + "name": "getTimeout", + "parameterTypes": [] + }, + { + "name": "hashCode", + "parameterTypes": [] + }, + { + "name": "isReadOnly", + "parameterTypes": [] + }, + { + "name": "setIsolationLevel", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setIsolationLevelName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPropagationBehavior", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setPropagationBehaviorName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setReadOnly", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setTimeout", + "parameterTypes": [ + "int" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.transaction.support.ResourceTransactionManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.transaction.support.TransactionOperations", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"executeWithoutResult","parameterTypes":["java.util.function.Consumer"] }] +}, +{ + "name":"org.springframework.transaction.support.TransactionTemplate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "equals", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "org.springframework.transaction.support.TransactionCallback" + ] + } + ] +}, +{ + "name":"org.springframework.util.AntPathMatcher", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "combine", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "extractPathWithinPattern", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "extractUriTemplateVariables", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "getPatternComparator", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "isPattern", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "match", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "matchStart", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.util.Assert" +}, +{ + "name":"org.springframework.util.CustomizableThreadCreator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "createThread", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "getThreadGroup", + "parameterTypes": [] + }, + { + "name": "getThreadNamePrefix", + "parameterTypes": [] + }, + { + "name": "getThreadPriority", + "parameterTypes": [] + }, + { + "name": "isDaemon", + "parameterTypes": [] + }, + { + "name": "setDaemon", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setThreadGroup", + "parameterTypes": [ + "java.lang.ThreadGroup" + ] + }, + { + "name": "setThreadGroupName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setThreadPriority", + "parameterTypes": [ + "int" + ] + } + ] +}, +{ + "name":"org.springframework.util.PathMatcher", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.validation.SmartValidator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.validation.Validator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.validation.beanvalidation.LocalValidatorFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "close", + "parameterTypes": [] + }, + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "getClockProvider", + "parameterTypes": [] + }, + { + "name": "getConstraintValidatorFactory", + "parameterTypes": [] + }, + { + "name": "getMessageInterpolator", + "parameterTypes": [] + }, + { + "name": "getParameterNameProvider", + "parameterTypes": [] + }, + { + "name": "getTraversableResolver", + "parameterTypes": [] + }, + { + "name": "getValidationPropertyMap", + "parameterTypes": [] + }, + { + "name": "getValidator", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setConfigurationInitializer", + "parameterTypes": [ + "java.util.function.Consumer" + ] + }, + { + "name": "setConstraintValidatorFactory", + "parameterTypes": [ + "jakarta.validation.ConstraintValidatorFactory" + ] + }, + { + "name": "setMappingLocations", + "parameterTypes": [ + "org.springframework.core.io.Resource[]" + ] + }, + { + "name": "setMessageInterpolator", + "parameterTypes": [ + "jakarta.validation.MessageInterpolator" + ] + }, + { + "name": "setParameterNameDiscoverer", + "parameterTypes": [ + "org.springframework.core.ParameterNameDiscoverer" + ] + }, + { + "name": "setProviderClass", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setTraversableResolver", + "parameterTypes": [ + "jakarta.validation.TraversableResolver" + ] + }, + { + "name": "setValidationMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + }, + { + "name": "setValidationProperties", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setValidationPropertyMap", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setValidationProviderResolver", + "parameterTypes": [ + "jakarta.validation.ValidationProviderResolver" + ] + }, + { + "name": "unwrap", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "usingContext", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.validation.beanvalidation.MethodValidationPostProcessor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "setValidatedAnnotationType", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setValidator", + "parameterTypes": [ + "jakarta.validation.Validator" + ] + }, + { + "name": "setValidatorFactory", + "parameterTypes": [ + "jakarta.validation.ValidatorFactory" + ] + }, + { + "name": "setValidatorProvider", + "parameterTypes": [ + "org.springframework.beans.factory.ObjectProvider" + ] + } + ] +}, +{ + "name":"org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"afterPropertiesSet","parameterTypes":[] }] +}, +{ + "name":"org.springframework.validation.beanvalidation.SpringValidatorAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "forExecutables", + "parameterTypes": [] + }, + { + "name": "getConstraintsForClass", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "validate", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.validation.Errors" + ] + }, + { + "name": "validate", + "parameterTypes": [ + "java.lang.Object", + "org.springframework.validation.Errors", + "java.lang.Object[]" + ] + }, + { + "name": "validate", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Class[]" + ] + }, + { + "name": "validateProperty", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String", + "java.lang.Class[]" + ] + }, + { + "name": "validateValue", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String", + "java.lang.Object", + "org.springframework.validation.Errors", + "java.lang.Object[]" + ] + }, + { + "name": "validateValue", + "parameterTypes": [ + "java.lang.Class", + "java.lang.String", + "java.lang.Object", + "java.lang.Class[]" + ] + } + ] +}, +{ + "name":"org.springframework.web.accept.ContentNegotiationManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getAllFileExtensions", + "parameterTypes": [] + }, + { + "name": "resolveFileExtensions", + "parameterTypes": [ + "org.springframework.http.MediaType" + ] + }, + { + "name": "resolveMediaTypes", + "parameterTypes": [ + "org.springframework.web.context.request.NativeWebRequest" + ] + } + ] +}, +{ + "name":"org.springframework.web.accept.ContentNegotiationStrategy", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.accept.MediaTypeFileExtensionResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.ControllerAdvice", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.CrossOrigin", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.ExceptionHandler", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.InitBinder", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.Mapping", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.PathVariable", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.RequestBody", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.RequestMapping", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.RequestMethod" +}, +{ + "name":"org.springframework.web.bind.annotation.RequestParam", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.ResponseBody", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.ResponseStatus", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.bind.annotation.RestController", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.client.RestOperations", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.client.RestTemplate", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "delete", + "parameterTypes": [ + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "delete", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]" + ] + }, + { + "name": "delete", + "parameterTypes": [ + "java.net.URI" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "java.lang.String", + "org.springframework.http.HttpMethod", + "org.springframework.http.HttpEntity", + "java.lang.Class", + "java.util.Map" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "java.lang.String", + "org.springframework.http.HttpMethod", + "org.springframework.http.HttpEntity", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "java.lang.String", + "org.springframework.http.HttpMethod", + "org.springframework.http.HttpEntity", + "org.springframework.core.ParameterizedTypeReference", + "java.util.Map" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "java.lang.String", + "org.springframework.http.HttpMethod", + "org.springframework.http.HttpEntity", + "org.springframework.core.ParameterizedTypeReference", + "java.lang.Object[]" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "java.net.URI", + "org.springframework.http.HttpMethod", + "org.springframework.http.HttpEntity", + "java.lang.Class" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "java.net.URI", + "org.springframework.http.HttpMethod", + "org.springframework.http.HttpEntity", + "org.springframework.core.ParameterizedTypeReference" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "org.springframework.http.RequestEntity", + "java.lang.Class" + ] + }, + { + "name": "exchange", + "parameterTypes": [ + "org.springframework.http.RequestEntity", + "org.springframework.core.ParameterizedTypeReference" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String", + "org.springframework.http.HttpMethod", + "org.springframework.web.client.RequestCallback", + "org.springframework.web.client.ResponseExtractor", + "java.util.Map" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.lang.String", + "org.springframework.http.HttpMethod", + "org.springframework.web.client.RequestCallback", + "org.springframework.web.client.ResponseExtractor", + "java.lang.Object[]" + ] + }, + { + "name": "execute", + "parameterTypes": [ + "java.net.URI", + "org.springframework.http.HttpMethod", + "org.springframework.web.client.RequestCallback", + "org.springframework.web.client.ResponseExtractor" + ] + }, + { + "name": "getForEntity", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class", + "java.util.Map" + ] + }, + { + "name": "getForEntity", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "getForEntity", + "parameterTypes": [ + "java.net.URI", + "java.lang.Class" + ] + }, + { + "name": "getForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class", + "java.util.Map" + ] + }, + { + "name": "getForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "getForObject", + "parameterTypes": [ + "java.net.URI", + "java.lang.Class" + ] + }, + { + "name": "headForHeaders", + "parameterTypes": [ + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "headForHeaders", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]" + ] + }, + { + "name": "headForHeaders", + "parameterTypes": [ + "java.net.URI" + ] + }, + { + "name": "optionsForAllow", + "parameterTypes": [ + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "optionsForAllow", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object[]" + ] + }, + { + "name": "optionsForAllow", + "parameterTypes": [ + "java.net.URI" + ] + }, + { + "name": "patchForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Class", + "java.util.Map" + ] + }, + { + "name": "patchForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "patchForObject", + "parameterTypes": [ + "java.net.URI", + "java.lang.Object", + "java.lang.Class" + ] + }, + { + "name": "postForEntity", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Class", + "java.util.Map" + ] + }, + { + "name": "postForEntity", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "postForEntity", + "parameterTypes": [ + "java.net.URI", + "java.lang.Object", + "java.lang.Class" + ] + }, + { + "name": "postForLocation", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.util.Map" + ] + }, + { + "name": "postForLocation", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Object[]" + ] + }, + { + "name": "postForLocation", + "parameterTypes": [ + "java.net.URI", + "java.lang.Object" + ] + }, + { + "name": "postForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Class", + "java.util.Map" + ] + }, + { + "name": "postForObject", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Class", + "java.lang.Object[]" + ] + }, + { + "name": "postForObject", + "parameterTypes": [ + "java.net.URI", + "java.lang.Object", + "java.lang.Class" + ] + }, + { + "name": "put", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.util.Map" + ] + }, + { + "name": "put", + "parameterTypes": [ + "java.lang.String", + "java.lang.Object", + "java.lang.Object[]" + ] + }, + { + "name": "put", + "parameterTypes": [ + "java.net.URI", + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.web.context.ConfigurableWebApplicationContext" +}, +{ + "name":"org.springframework.web.context.ServletContextAware", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.context.WebApplicationContext" +}, +{ + "name":"org.springframework.web.context.annotation.RequestScope", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.context.support.GenericWebApplicationContext" +}, +{ + "name":"org.springframework.web.context.support.WebApplicationObjectSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setServletContext","parameterTypes":["jakarta.servlet.ServletContext"] }] +}, +{ + "name":"org.springframework.web.filter.CharacterEncodingFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getEncoding", + "parameterTypes": [] + }, + { + "name": "isForceRequestEncoding", + "parameterTypes": [] + }, + { + "name": "isForceResponseEncoding", + "parameterTypes": [] + }, + { + "name": "setEncoding", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setForceEncoding", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setForceRequestEncoding", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setForceResponseEncoding", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.web.filter.DelegatingFilterProxy", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.filter.GenericFilterBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "getEnvironment", + "parameterTypes": [] + }, + { + "name": "getFilterConfig", + "parameterTypes": [] + }, + { + "name": "init", + "parameterTypes": [ + "jakarta.servlet.FilterConfig" + ] + }, + { + "name": "setBeanName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setEnvironment", + "parameterTypes": [ + "org.springframework.core.env.Environment" + ] + }, + { + "name": "setServletContext", + "parameterTypes": [ + "jakarta.servlet.ServletContext" + ] + } + ] +}, +{ + "name":"org.springframework.web.filter.OncePerRequestFilter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"doFilter","parameterTypes":["jakarta.servlet.ServletRequest","jakarta.servlet.ServletResponse","jakarta.servlet.FilterChain"] }] +}, +{ + "name":"org.springframework.web.filter.ServerHttpObservationFilter", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.method.annotation.ExceptionHandlerMethodResolver", + "methods":[{"name":"noMatchingExceptionHandler","parameterTypes":[] }] +}, +{ + "name":"org.springframework.web.method.support.CompositeUriComponentsContributor", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "contributeMethodArgument", + "parameterTypes": [ + "org.springframework.core.MethodParameter", + "java.lang.Object", + "org.springframework.web.util.UriComponentsBuilder", + "java.util.Map", + "org.springframework.core.convert.ConversionService" + ] + }, + { + "name": "supportsParameter", + "parameterTypes": [ + "org.springframework.core.MethodParameter" + ] + } + ] +}, +{ + "name":"org.springframework.web.method.support.HandlerMethodArgumentResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.method.support.UriComponentsContributor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.multipart.MultipartResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.multipart.support.StandardServletMultipartResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "cleanupMultipart", + "parameterTypes": [ + "org.springframework.web.multipart.MultipartHttpServletRequest" + ] + }, + { + "name": "isMultipart", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "resolveMultipart", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + } + ] +}, +{ + "name":"org.springframework.web.reactive$DispatcherHandler" +}, +{ + "name":"org.springframework.web.reactive.DispatcherHandler" +}, +{ + "name":"org.springframework.web.reactive.HandlerResult" +}, +{ + "name":"org.springframework.web.reactive.config$EnableWebFlux" +}, +{ + "name":"org.springframework.web.reactive.config.EnableWebFlux" +}, +{ + "name":"org.springframework.web.reactive.config.WebFluxConfigurer" +}, +{ + "name":"org.springframework.web.reactive.function.client$ExchangeFilterFunction" +}, +{ + "name":"org.springframework.web.reactive.function.client.ExchangeFilterFunction" +}, +{ + "name":"org.springframework.web.reactive.function.client.WebClient" +}, +{ + "name":"org.springframework.web.reactive.result.view.View" +}, +{ + "name":"org.springframework.web.servlet.DispatcherServlet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.FlashMapManager", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.FrameworkServlet", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "destroy", + "parameterTypes": [] + }, + { + "name": "getContextAttribute", + "parameterTypes": [] + }, + { + "name": "getContextClass", + "parameterTypes": [] + }, + { + "name": "getContextConfigLocation", + "parameterTypes": [] + }, + { + "name": "getContextId", + "parameterTypes": [] + }, + { + "name": "getNamespace", + "parameterTypes": [] + }, + { + "name": "getServletContextAttributeName", + "parameterTypes": [] + }, + { + "name": "getWebApplicationContext", + "parameterTypes": [] + }, + { + "name": "isEnableLoggingRequestDetails", + "parameterTypes": [] + }, + { + "name": "onApplicationEvent", + "parameterTypes": [ + "org.springframework.context.event.ContextRefreshedEvent" + ] + }, + { + "name": "refresh", + "parameterTypes": [] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setContextAttribute", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setContextClass", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "setContextConfigLocation", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setContextId", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setContextInitializerClasses", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setContextInitializers", + "parameterTypes": [ + "org.springframework.context.ApplicationContextInitializer[]" + ] + }, + { + "name": "setDispatchOptionsRequest", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setDispatchTraceRequest", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setEnableLoggingRequestDetails", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setNamespace", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPublishContext", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setPublishEvents", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setThreadContextInheritable", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.HandlerAdapter", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.HandlerExceptionResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.HandlerInterceptor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterCompletion", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object", + "java.lang.Exception" + ] + }, + { + "name": "postHandle", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object", + "org.springframework.web.servlet.ModelAndView" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.HandlerMapping", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.HttpServletBean", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getEnvironment", + "parameterTypes": [] + }, + { + "name": "getServletName", + "parameterTypes": [] + }, + { + "name": "init", + "parameterTypes": [] + }, + { + "name": "setEnvironment", + "parameterTypes": [ + "org.springframework.core.env.Environment" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.LocaleResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.RequestToViewNameTranslator", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.ThemeResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name": "org.springframework.web.servlet.View" +}, +{ + "name":"org.springframework.web.servlet.ViewResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.config.annotation.EnableWebMvc", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "beanNameHandlerMapping", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "defaultServletHandlerMapping", + "parameterTypes": [] + }, + { + "name": "flashMapManager", + "parameterTypes": [] + }, + { + "name": "getApplicationContext", + "parameterTypes": [] + }, + { + "name": "getServletContext", + "parameterTypes": [] + }, + { + "name": "handlerExceptionResolver", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager" + ] + }, + { + "name": "handlerFunctionAdapter", + "parameterTypes": [] + }, + { + "name": "httpRequestHandlerAdapter", + "parameterTypes": [] + }, + { + "name": "localeResolver", + "parameterTypes": [] + }, + { + "name": "mvcContentNegotiationManager", + "parameterTypes": [] + }, + { + "name": "mvcConversionService", + "parameterTypes": [] + }, + { + "name": "mvcHandlerMappingIntrospector", + "parameterTypes": [] + }, + { + "name": "mvcPathMatcher", + "parameterTypes": [] + }, + { + "name": "mvcPatternParser", + "parameterTypes": [] + }, + { + "name": "mvcResourceUrlProvider", + "parameterTypes": [] + }, + { + "name": "mvcUriComponentsContributor", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" + ] + }, + { + "name": "mvcUrlPathHelper", + "parameterTypes": [] + }, + { + "name": "mvcValidator", + "parameterTypes": [] + }, + { + "name": "mvcViewResolver", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager" + ] + }, + { + "name": "requestMappingHandlerAdapter", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager", + "org.springframework.format.support.FormattingConversionService", + "org.springframework.validation.Validator" + ] + }, + { + "name": "resourceHandlerMapping", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager", + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "routerFunctionMapping", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setServletContext", + "parameterTypes": [ + "jakarta.servlet.ServletContext" + ] + }, + { + "name": "simpleControllerHandlerAdapter", + "parameterTypes": [] + }, + { + "name": "themeResolver", + "parameterTypes": [] + }, + { + "name": "viewControllerHandlerMapping", + "parameterTypes": [ + "org.springframework.format.support.FormattingConversionService", + "org.springframework.web.servlet.resource.ResourceUrlProvider" + ] + }, + { + "name": "viewNameTranslator", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport$$SpringCGLIB$$0", + "methods":[{"name":"","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"org.springframework.web.servlet.config.annotation.WebMvcConfigurer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addArgumentResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "addCorsMappings", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.CorsRegistry" + ] + }, + { + "name": "addFormatters", + "parameterTypes": [ + "org.springframework.format.FormatterRegistry" + ] + }, + { + "name": "addInterceptors", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.InterceptorRegistry" + ] + }, + { + "name": "addResourceHandlers", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry" + ] + }, + { + "name": "addReturnValueHandlers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "addViewControllers", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.ViewControllerRegistry" + ] + }, + { + "name": "configureAsyncSupport", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer" + ] + }, + { + "name": "configureContentNegotiation", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer" + ] + }, + { + "name": "configureDefaultServletHandling", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer" + ] + }, + { + "name": "configureHandlerExceptionResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "configureMessageConverters", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "configurePathMatch", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.PathMatchConfigurer" + ] + }, + { + "name": "configureViewResolvers", + "parameterTypes": [ + "org.springframework.web.servlet.config.annotation.ViewResolverRegistry" + ] + }, + { + "name": "extendHandlerExceptionResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "extendMessageConverters", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "getMessageCodesResolver", + "parameterTypes": [] + }, + { + "name": "getValidator", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.servlet.function.support.HandlerFunctionAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getLastModified", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.Object" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "handle", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.function.support.RouterFunctionMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"afterPropertiesSet","parameterTypes":[] }] +}, +{ + "name":"org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "initApplicationContext", + "parameterTypes": [] + }, + { + "name": "setDetectHandlersInAncestorContexts", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.handler.AbstractHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getCorsConfigurationSource", + "parameterTypes": [] + }, + { + "name": "getCorsProcessor", + "parameterTypes": [] + }, + { + "name": "getDefaultHandler", + "parameterTypes": [] + }, + { + "name": "getHandler", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "getPathMatcher", + "parameterTypes": [] + }, + { + "name": "getPatternParser", + "parameterTypes": [] + }, + { + "name": "getUrlPathHelper", + "parameterTypes": [] + }, + { + "name": "setAlwaysUseFullPath", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setBeanName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setCorsConfigurationSource", + "parameterTypes": [ + "org.springframework.web.cors.CorsConfigurationSource" + ] + }, + { + "name": "setCorsConfigurations", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setCorsProcessor", + "parameterTypes": [ + "org.springframework.web.cors.CorsProcessor" + ] + }, + { + "name": "setDefaultHandler", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setInterceptors", + "parameterTypes": [ + "java.lang.Object[]" + ] + }, + { + "name": "setOrder", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setPathMatcher", + "parameterTypes": [ + "org.springframework.util.PathMatcher" + ] + }, + { + "name": "setPatternParser", + "parameterTypes": [ + "org.springframework.web.util.pattern.PathPatternParser" + ] + }, + { + "name": "setRemoveSemicolonContent", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUrlDecode", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUrlPathHelper", + "parameterTypes": [ + "org.springframework.web.util.UrlPathHelper" + ] + }, + { + "name": "usesPathPatterns", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.servlet.handler.AbstractHandlerMethodMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getDirectPaths", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getHandlerMethods", + "parameterTypes": [] + }, + { + "name": "getHandlerMethodsForMappingName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getMappingPathPatterns", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "getMatchingMapping", + "parameterTypes": [ + "java.lang.Object", + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "getNamingStrategy", + "parameterTypes": [] + }, + { + "name": "handleMatch", + "parameterTypes": [ + "java.lang.Object", + "java.lang.String", + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "initCorsConfiguration", + "parameterTypes": [ + "java.lang.Object", + "java.lang.reflect.Method", + "java.lang.Object" + ] + }, + { + "name": "registerHandlerMethod", + "parameterTypes": [ + "java.lang.Object", + "java.lang.reflect.Method", + "java.lang.Object" + ] + }, + { + "name": "registerMapping", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Object", + "java.lang.reflect.Method" + ] + }, + { + "name": "setDetectHandlerMethodsInAncestorContexts", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setHandlerMethodMappingNamingStrategy", + "parameterTypes": [ + "org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy" + ] + }, + { + "name": "setPatternParser", + "parameterTypes": [ + "org.springframework.web.util.pattern.PathPatternParser" + ] + }, + { + "name": "unregisterMapping", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$EmptyHandler", + "methods":[{"name":"handle","parameterTypes":[] }] +}, +{ + "name":"org.springframework.web.servlet.handler.AbstractUrlHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getHandlerMap", + "parameterTypes": [] + }, + { + "name": "getPathPatternHandlerMap", + "parameterTypes": [] + }, + { + "name": "getRootHandler", + "parameterTypes": [] + }, + { + "name": "match", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.String" + ] + }, + { + "name": "setLazyInitHandlers", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setPatternParser", + "parameterTypes": [ + "org.springframework.web.util.pattern.PathPatternParser" + ] + }, + { + "name": "setRootHandler", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setUseTrailingSlashMatch", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "useTrailingSlashMatch", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.handler.HandlerExceptionResolverComposite", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "resolveException", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object", + "java.lang.Exception" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.handler.HandlerMappingIntrospector" +}, +{ + "name":"org.springframework.web.servlet.handler.MatchableHandlerMapping", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.handler.SimpleUrlHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getUrlMap", + "parameterTypes": [] + }, + { + "name": "initApplicationContext", + "parameterTypes": [] + }, + { + "name": "setMappings", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setUrlMap", + "parameterTypes": [ + "java.util.Map" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.i18n.AbstractLocaleResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setDefaultLocale","parameterTypes":["java.util.Locale"] }] +}, +{ + "name":"org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "resolveLocale", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "setLocale", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.util.Locale" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getLastModified", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.Object" + ] + }, + { + "name": "handle", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "getLastModified", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.Object" + ] + }, + { + "name": "handle", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getLastModified", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.Object" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "handle", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.Object" + ] + }, + { + "name": "setOrder", + "parameterTypes": [ + "int" + ] + }, + { + "name": "supports", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler", + "methods":[{"name":"handle","parameterTypes":[] }] +}, +{ + "name":"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "setBeanFactory", + "parameterTypes": [ + "org.springframework.beans.factory.BeanFactory" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getBuilderConfiguration", + "parameterTypes": [] + }, + { + "name": "getContentNegotiationManager", + "parameterTypes": [] + }, + { + "name": "getFileExtensions", + "parameterTypes": [] + }, + { + "name": "getPathPrefixes", + "parameterTypes": [] + }, + { + "name": "initCorsConfiguration", + "parameterTypes": [ + "java.lang.Object", + "java.lang.reflect.Method", + "java.lang.Object" + ] + }, + { + "name": "match", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "java.lang.String" + ] + }, + { + "name": "registerHandlerMethod", + "parameterTypes": [ + "java.lang.Object", + "java.lang.reflect.Method", + "java.lang.Object" + ] + }, + { + "name": "registerMapping", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Object", + "java.lang.reflect.Method" + ] + }, + { + "name": "registerMapping", + "parameterTypes": [ + "org.springframework.web.servlet.mvc.method.RequestMappingInfo", + "java.lang.Object", + "java.lang.reflect.Method" + ] + }, + { + "name": "setContentNegotiationManager", + "parameterTypes": [ + "org.springframework.web.accept.ContentNegotiationManager" + ] + }, + { + "name": "setEmbeddedValueResolver", + "parameterTypes": [ + "org.springframework.util.StringValueResolver" + ] + }, + { + "name": "setPathPrefixes", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setPatternParser", + "parameterTypes": [ + "org.springframework.web.util.pattern.PathPatternParser" + ] + }, + { + "name": "setUseRegisteredSuffixPatternMatch", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUseSuffixPatternMatch", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUseTrailingSlashMatch", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "useRegisteredSuffixPatternMatch", + "parameterTypes": [] + }, + { + "name": "useSuffixPatternMatch", + "parameterTypes": [] + }, + { + "name": "useTrailingSlashMatch", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.servlet.resource.ResourceUrlProvider", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "onApplicationEvent", + "parameterTypes": [ + "org.springframework.context.ApplicationEvent" + ] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.support.AbstractFlashMapManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getFlashMapTimeout", + "parameterTypes": [] + }, + { + "name": "getUrlPathHelper", + "parameterTypes": [] + }, + { + "name": "retrieveAndUpdate", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse" + ] + }, + { + "name": "saveOutputFlashMap", + "parameterTypes": [ + "org.springframework.web.servlet.FlashMap", + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse" + ] + }, + { + "name": "setFlashMapTimeout", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setUrlPathHelper", + "parameterTypes": [ + "org.springframework.web.util.UrlPathHelper" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.support.RequestDataValueProcessor", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.support.SessionFlashMapManager", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.servlet.support.WebContentGenerator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getCacheControl", + "parameterTypes": [] + }, + { + "name": "getCacheSeconds", + "parameterTypes": [] + }, + { + "name": "getSupportedMethods", + "parameterTypes": [] + }, + { + "name": "getVaryByRequestHeaders", + "parameterTypes": [] + }, + { + "name": "isAlwaysMustRevalidate", + "parameterTypes": [] + }, + { + "name": "isRequireSession", + "parameterTypes": [] + }, + { + "name": "isUseCacheControlHeader", + "parameterTypes": [] + }, + { + "name": "isUseCacheControlNoStore", + "parameterTypes": [] + }, + { + "name": "isUseExpiresHeader", + "parameterTypes": [] + }, + { + "name": "setAlwaysMustRevalidate", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setCacheControl", + "parameterTypes": [ + "org.springframework.http.CacheControl" + ] + }, + { + "name": "setCacheSeconds", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setRequireSession", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setSupportedMethods", + "parameterTypes": [ + "java.lang.String[]" + ] + }, + { + "name": "setUseCacheControlHeader", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUseCacheControlNoStore", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setUseExpiresHeader", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setVaryByRequestHeaders", + "parameterTypes": [ + "java.lang.String[]" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.theme.AbstractThemeResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getDefaultThemeName", + "parameterTypes": [] + }, + { + "name": "setDefaultThemeName", + "parameterTypes": [ + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.theme.FixedThemeResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "resolveThemeName", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest" + ] + }, + { + "name": "setThemeName", + "parameterTypes": [ + "jakarta.servlet.http.HttpServletRequest", + "jakarta.servlet.http.HttpServletResponse", + "java.lang.String" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.view.AbstractCachingViewResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "clearCache", + "parameterTypes": [] + }, + { + "name": "getCacheFilter", + "parameterTypes": [] + }, + { + "name": "getCacheLimit", + "parameterTypes": [] + }, + { + "name": "isCache", + "parameterTypes": [] + }, + { + "name": "isCacheUnresolved", + "parameterTypes": [] + }, + { + "name": "removeFromCache", + "parameterTypes": [ + "java.lang.String", + "java.util.Locale" + ] + }, + { + "name": "resolveViewName", + "parameterTypes": [ + "java.lang.String", + "java.util.Locale" + ] + }, + { + "name": "setCache", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setCacheFilter", + "parameterTypes": [ + "org.springframework.web.servlet.view.AbstractCachingViewResolver$CacheFilter" + ] + }, + { + "name": "setCacheLimit", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setCacheUnresolved", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getViewName","parameterTypes":["jakarta.servlet.http.HttpServletRequest"] }] +}, +{ + "name":"org.springframework.web.servlet.view.ViewResolverComposite", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterPropertiesSet", + "parameterTypes": [] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "resolveViewName", + "parameterTypes": [ + "java.lang.String", + "java.util.Locale" + ] + }, + { + "name": "setApplicationContext", + "parameterTypes": [ + "org.springframework.context.ApplicationContext" + ] + }, + { + "name": "setServletContext", + "parameterTypes": [ + "jakarta.servlet.ServletContext" + ] + } + ] +}, +{ + "name":"org.springframework.web.socket.SubProtocolCapable", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.socket.WebSocketHandler", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.socket.config.WebSocketMessageBrokerStats", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"toString","parameterTypes":[] }] +}, +{ + "name":"org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setConfigurers", + "parameterTypes": [ + "java.util.List" + ] + } + ] +}, +{ + "name":"org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker", + "queryAllDeclaredMethods":true +}, +{ + "name":"org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurationSupport", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "stompWebSocketHandlerMapping", + "parameterTypes": [ + "org.springframework.web.socket.WebSocketHandler", + "org.springframework.scheduling.TaskScheduler" + ] + }, + { + "name": "subProtocolWebSocketHandler", + "parameterTypes": [ + "org.springframework.messaging.support.AbstractSubscribableChannel", + "org.springframework.messaging.support.AbstractSubscribableChannel" + ] + }, + { + "name": "webSocketMessageBrokerStats", + "parameterTypes": [ + "org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler", + "org.springframework.web.socket.WebSocketHandler", + "org.springframework.core.task.TaskExecutor", + "org.springframework.core.task.TaskExecutor", + "org.springframework.scheduling.TaskScheduler" + ] + }, + { + "name": "webSocketScopeConfigurer", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "addArgumentResolvers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "addReturnValueHandlers", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "configureClientInboundChannel", + "parameterTypes": [ + "org.springframework.messaging.simp.config.ChannelRegistration" + ] + }, + { + "name": "configureClientOutboundChannel", + "parameterTypes": [ + "org.springframework.messaging.simp.config.ChannelRegistration" + ] + }, + { + "name": "configureMessageBroker", + "parameterTypes": [ + "org.springframework.messaging.simp.config.MessageBrokerRegistry" + ] + }, + { + "name": "configureMessageConverters", + "parameterTypes": [ + "java.util.List" + ] + }, + { + "name": "configureWebSocketTransport", + "parameterTypes": [ + "org.springframework.web.socket.config.annotation.WebSocketTransportRegistration" + ] + }, + { + "name": "registerStompEndpoints", + "parameterTypes": [ + "org.springframework.web.socket.config.annotation.StompEndpointRegistry" + ] + } + ] +}, +{ + "name":"org.springframework.web.socket.messaging.DefaultSimpUserRegistry", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "findSubscriptions", + "parameterTypes": [ + "org.springframework.messaging.simp.user.SimpSubscriptionMatcher" + ] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "getUser", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getUserCount", + "parameterTypes": [] + }, + { + "name": "getUsers", + "parameterTypes": [] + }, + { + "name": "onApplicationEvent", + "parameterTypes": [ + "org.springframework.context.ApplicationEvent" + ] + }, + { + "name": "supportsEventType", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "supportsSourceType", + "parameterTypes": [ + "java.lang.Class" + ] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.socket.messaging.SubProtocolWebSocketHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "afterConnectionClosed", + "parameterTypes": [ + "org.springframework.web.socket.WebSocketSession", + "org.springframework.web.socket.CloseStatus" + ] + }, + { + "name": "afterConnectionEstablished", + "parameterTypes": [ + "org.springframework.web.socket.WebSocketSession" + ] + }, + { + "name": "getSubProtocols", + "parameterTypes": [] + }, + { + "name": "handleMessage", + "parameterTypes": [ + "org.springframework.messaging.Message" + ] + }, + { + "name": "handleMessage", + "parameterTypes": [ + "org.springframework.web.socket.WebSocketSession", + "org.springframework.web.socket.WebSocketMessage" + ] + }, + { + "name": "handleTransportError", + "parameterTypes": [ + "org.springframework.web.socket.WebSocketSession", + "java.lang.Throwable" + ] + }, + { + "name": "isRunning", + "parameterTypes": [] + }, + { + "name": "start", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [ + "java.lang.Runnable" + ] + }, + { + "name": "supportsPartialMessages", + "parameterTypes": [] + }, + { + "name": "toString", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.socket.messaging.WebSocketAnnotationMethodMessageHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"afterPropertiesSet","parameterTypes":[] }] +}, +{ + "name":"org.springframework.web.socket.server.support.WebSocketHandlerMapping", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "isRunning", + "parameterTypes": [] + }, + { + "name": "start", + "parameterTypes": [] + }, + { + "name": "stop", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.springframework.web.util.UrlPathHelper", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.web.util.pattern.PathPatternParser", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.springframework.ws.client.core.WebServiceTemplate" +}, +{ + "name":"org.springframework.ws.transport.http.MessageDispatcherServlet" +}, +{ + "name":"org.thymeleaf.ITemplateEngine", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.thymeleaf.TemplateEngine", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addDialect", + "parameterTypes": [ + "java.lang.String", + "org.thymeleaf.dialect.IDialect" + ] + }, + { + "name": "addDialect", + "parameterTypes": [ + "org.thymeleaf.dialect.IDialect" + ] + }, + { + "name": "addLinkBuilder", + "parameterTypes": [ + "org.thymeleaf.linkbuilder.ILinkBuilder" + ] + }, + { + "name": "addMessageResolver", + "parameterTypes": [ + "org.thymeleaf.messageresolver.IMessageResolver" + ] + }, + { + "name": "addTemplateResolver", + "parameterTypes": [ + "org.thymeleaf.templateresolver.ITemplateResolver" + ] + }, + { + "name": "clearDialects", + "parameterTypes": [] + }, + { + "name": "clearTemplateCache", + "parameterTypes": [] + }, + { + "name": "clearTemplateCacheFor", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "getCacheManager", + "parameterTypes": [] + }, + { + "name": "getConfiguration", + "parameterTypes": [] + }, + { + "name": "getDecoupledTemplateLogicResolver", + "parameterTypes": [] + }, + { + "name": "getDialects", + "parameterTypes": [] + }, + { + "name": "getDialectsByPrefix", + "parameterTypes": [] + }, + { + "name": "getEngineContextFactory", + "parameterTypes": [] + }, + { + "name": "getLinkBuilders", + "parameterTypes": [] + }, + { + "name": "getMessageResolvers", + "parameterTypes": [] + }, + { + "name": "getTemplateResolvers", + "parameterTypes": [] + }, + { + "name": "isInitialized", + "parameterTypes": [] + }, + { + "name": "process", + "parameterTypes": [ + "java.lang.String", + "java.util.Set", + "org.thymeleaf.context.IContext" + ] + }, + { + "name": "process", + "parameterTypes": [ + "java.lang.String", + "java.util.Set", + "org.thymeleaf.context.IContext", + "java.io.Writer" + ] + }, + { + "name": "process", + "parameterTypes": [ + "java.lang.String", + "org.thymeleaf.context.IContext" + ] + }, + { + "name": "process", + "parameterTypes": [ + "java.lang.String", + "org.thymeleaf.context.IContext", + "java.io.Writer" + ] + }, + { + "name": "process", + "parameterTypes": [ + "org.thymeleaf.TemplateSpec", + "org.thymeleaf.context.IContext" + ] + }, + { + "name": "process", + "parameterTypes": [ + "org.thymeleaf.TemplateSpec", + "org.thymeleaf.context.IContext", + "java.io.Writer" + ] + }, + { + "name": "processThrottled", + "parameterTypes": [ + "java.lang.String", + "java.util.Set", + "org.thymeleaf.context.IContext" + ] + }, + { + "name": "processThrottled", + "parameterTypes": [ + "java.lang.String", + "org.thymeleaf.context.IContext" + ] + }, + { + "name": "processThrottled", + "parameterTypes": [ + "org.thymeleaf.TemplateSpec", + "org.thymeleaf.context.IContext" + ] + }, + { + "name": "setAdditionalDialects", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setCacheManager", + "parameterTypes": [ + "org.thymeleaf.cache.ICacheManager" + ] + }, + { + "name": "setDecoupledTemplateLogicResolver", + "parameterTypes": [ + "org.thymeleaf.templateparser.markup.decoupled.IDecoupledTemplateLogicResolver" + ] + }, + { + "name": "setDialect", + "parameterTypes": [ + "org.thymeleaf.dialect.IDialect" + ] + }, + { + "name": "setDialects", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setDialectsByPrefix", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setEngineContextFactory", + "parameterTypes": [ + "org.thymeleaf.context.IEngineContextFactory" + ] + }, + { + "name": "setLinkBuilder", + "parameterTypes": [ + "org.thymeleaf.linkbuilder.ILinkBuilder" + ] + }, + { + "name": "setLinkBuilders", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setMessageResolver", + "parameterTypes": [ + "org.thymeleaf.messageresolver.IMessageResolver" + ] + }, + { + "name": "setMessageResolvers", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setTemplateResolver", + "parameterTypes": [ + "org.thymeleaf.templateresolver.ITemplateResolver" + ] + }, + { + "name": "setTemplateResolvers", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "threadIndex", + "parameterTypes": [] + } + ] +}, +{ + "name":"org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect" +}, +{ + "name":"org.thymeleaf.spring6.ISpringTemplateEngine", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.thymeleaf.spring6.SpringTemplateEngine", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods": [ + { + "name": "setMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + }, + { + "name": "setTemplateEngineMessageSource", + "parameterTypes": [ + "org.springframework.context.MessageSource" + ] + } + ] +}, +{ + "name":"org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"setApplicationContext","parameterTypes":["org.springframework.context.ApplicationContext"] }] +}, +{ + "name":"org.thymeleaf.spring6.view.ThymeleafViewResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"getOrder","parameterTypes":[] }] +}, +{ + "name":"org.thymeleaf.templatemode.TemplateMode" +}, +{ + "name":"org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "addTemplateAlias", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "name": "clearTemplateAliases", + "parameterTypes": [] + }, + { + "name": "getCSSTemplateModePatternSpec", + "parameterTypes": [] + }, + { + "name": "getCSSTemplateModePatterns", + "parameterTypes": [] + }, + { + "name": "getCacheTTLMs", + "parameterTypes": [] + }, + { + "name": "getCacheablePatternSpec", + "parameterTypes": [] + }, + { + "name": "getCacheablePatterns", + "parameterTypes": [] + }, + { + "name": "getCharacterEncoding", + "parameterTypes": [] + }, + { + "name": "getForceSuffix", + "parameterTypes": [] + }, + { + "name": "getForceTemplateMode", + "parameterTypes": [] + }, + { + "name": "getHtmlTemplateModePatternSpec", + "parameterTypes": [] + }, + { + "name": "getHtmlTemplateModePatterns", + "parameterTypes": [] + }, + { + "name": "getJavaScriptTemplateModePatternSpec", + "parameterTypes": [] + }, + { + "name": "getJavaScriptTemplateModePatterns", + "parameterTypes": [] + }, + { + "name": "getNonCacheablePatternSpec", + "parameterTypes": [] + }, + { + "name": "getNonCacheablePatterns", + "parameterTypes": [] + }, + { + "name": "getPrefix", + "parameterTypes": [] + }, + { + "name": "getRawTemplateModePatternSpec", + "parameterTypes": [] + }, + { + "name": "getRawTemplateModePatterns", + "parameterTypes": [] + }, + { + "name": "getSuffix", + "parameterTypes": [] + }, + { + "name": "getTemplateAliases", + "parameterTypes": [] + }, + { + "name": "getTemplateMode", + "parameterTypes": [] + }, + { + "name": "getTextTemplateModePatternSpec", + "parameterTypes": [] + }, + { + "name": "getTextTemplateModePatterns", + "parameterTypes": [] + }, + { + "name": "getXmlTemplateModePatternSpec", + "parameterTypes": [] + }, + { + "name": "getXmlTemplateModePatterns", + "parameterTypes": [] + }, + { + "name": "isCacheable", + "parameterTypes": [] + }, + { + "name": "setCSSTemplateModePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setCacheTTLMs", + "parameterTypes": [ + "java.lang.Long" + ] + }, + { + "name": "setCacheable", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setCacheablePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setCharacterEncoding", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setForceSuffix", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setForceTemplateMode", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setHtmlTemplateModePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setJavaScriptTemplateModePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setNonCacheablePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setPrefix", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setRawTemplateModePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setSuffix", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setTemplateAliases", + "parameterTypes": [ + "java.util.Map" + ] + }, + { + "name": "setTemplateMode", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setTemplateMode", + "parameterTypes": [ + "org.thymeleaf.templatemode.TemplateMode" + ] + }, + { + "name": "setTextTemplateModePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setXmlTemplateModePatterns", + "parameterTypes": [ + "java.util.Set" + ] + } + ] +}, +{ + "name":"org.thymeleaf.templateresolver.AbstractTemplateResolver", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods": [ + { + "name": "getCheckExistence", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getOrder", + "parameterTypes": [] + }, + { + "name": "getResolvablePatternSpec", + "parameterTypes": [] + }, + { + "name": "getResolvablePatterns", + "parameterTypes": [] + }, + { + "name": "getUseDecoupledLogic", + "parameterTypes": [] + }, + { + "name": "resolveTemplate", + "parameterTypes": [ + "org.thymeleaf.IEngineConfiguration", + "java.lang.String", + "java.lang.String", + "java.util.Map" + ] + }, + { + "name": "setCheckExistence", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setOrder", + "parameterTypes": [ + "java.lang.Integer" + ] + }, + { + "name": "setResolvablePatterns", + "parameterTypes": [ + "java.util.Set" + ] + }, + { + "name": "setUseDecoupledLogic", + "parameterTypes": [ + "boolean" + ] + } + ] +}, +{ + "name":"org.thymeleaf.templateresolver.ITemplateResolver", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.webjars$WebJarAssetLocator" +}, +{ + "name":"org.webjars.WebJarAssetLocator" +}, +{ + "name":"org.xnio.SslClientAuthMode" +}, +{ + "name":"reactor.core.publisher$Flux" +}, +{ + "name":"reactor.core.publisher$Mono" +}, +{ + "name":"reactor.core.publisher.Flux" +}, +{ + "name":"reactor.core.publisher.Mono" +}, +{ + "name":"reactor.netty.http.server.HttpServer" +}, +{ + "name":"reactor.tools.agent$ReactorDebugAgent" +}, +{ + "name":"reactor.tools.agent.ReactorDebugAgent" +}, +{ + "name":"reactor.util.lang$NonNullApi" +}, +{ + "name":"reactor.util.lang.NonNullApi" +}, +{ + "name":"scala$Option" +}, +{ + "name":"scala.Option" +}, +{ + "name":"sun.java2d.marlin.DMarlinRenderingEngine", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.management.ClassLoadingImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.CompilationImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.ManagementFactoryHelper$1", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.ManagementFactoryHelper$PlatformLoggingImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.MemoryImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.MemoryManagerImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.MemoryPoolImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.management.RuntimeImpl", + "queryAllPublicConstructors":true +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods": [ + { + "name": "getReflectionFactory", + "parameterTypes": [] + }, + { + "name": "newConstructorForSerialization", + "parameterTypes": [ + "java.lang.Class", + "java.lang.reflect.Constructor" + ] + } + ] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.DRBG", + "methods":[{"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.DSA$SHA224withDSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.DSA$SHA256withDSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.JavaKeyStore$DualFormatJKS", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.JavaKeyStore$JKS", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.MD5", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA2$SHA224", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA2$SHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA5$SHA384", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA5$SHA512", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SecureRandom", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.certpath.PKIXCertPathValidator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.PSSParameters", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAPSSSignature", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSASignature$SHA224withRSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSASignature$SHA256withRSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSASignature$SHA384withRSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSASignature$SHA512withRSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.KeyManagerFactoryImpl$SunX509", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.SSLContextImpl$TLSContext", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.util.ObjectIdentifier" +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificateExtensions" +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.IssuerAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.NetscapeCertTypeExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.PrivateKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, + { + "name": "sun.security.x509.SubjectAlternativeNameExtension", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Boolean", + "java.lang.Object" + ] + } + ] + }, + { + "name": "sun.security.x509.SubjectKeyIdentifierExtension", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Boolean", + "java.lang.Object" + ] + } + ] + }, + { + "name": "void", + "queryAllDeclaredMethods": true + }, + { + "name": "weblogic.websocket.tyrus$TyrusServletWriter" + }, + { + "name": "weblogic.websocket.tyrus.TyrusServletWriter" + }, + { + "name": "zipkin2.reporter.Sender" + } +] diff --git a/core/src/main/resources/META-INF/native-image/resource-config.json b/core/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 000000000..3b2b24c1a --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,2728 @@ +{ + "resources":{ + "includes":[ + { + "pattern": ".*config.*yml$" + }, + { + "pattern": "\\Qconfig/baseConfig.yml\\E" + }, + { + "pattern": ".*baseConfig.yml.*$" + }, + { + "pattern": ".*application.properties.*$" + }, + { + "pattern": ".*config/application.properties.*$" + }, + { + "pattern": ".*migration.*$" + }, + { + "pattern": "\\Qstatic/.*\\E" + }, + { + "pattern": "\\Qbanner.txt\\E" + }, + { + "pattern": "\\Qcacerts\\E" + }, + { + "pattern": "\\Qchangelog.json\\E" + }, + { + "pattern": "\\Qchangelog.yaml\\E" + }, + { + "pattern": "\\QwrapperHashes2.json\\E" + }, + { + "pattern": "\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" + }, + { + "pattern": "\\QMETA-INF/services/com.fasterxml.jackson.databind.Module\\E" + }, + { + "pattern": "\\QMETA-INF/services/jakarta.el.ExpressionFactory\\E" + }, + { + "pattern": "\\QMETA-INF/services/jakarta.persistence.spi.PersistenceProvider\\E" + }, + { + "pattern": "\\QMETA-INF/services/jakarta.validation.spi.ValidationProvider\\E" + }, + { + "pattern": "\\QMETA-INF/services/jakarta.websocket.server.ServerEndpointConfig$Configurator\\E" + }, + { + "pattern": "\\QMETA-INF/services/jakarta.xml.bind.JAXBContextFactory\\E" + }, + { + "pattern": "\\QMETA-INF/services/java.nio.file.spi.FileSystemProvider\\E" + }, + { + "pattern": "\\QMETA-INF/services/java.sql.Driver\\E" + }, + { + "pattern": "\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E" + }, + { + "pattern": "\\QMETA-INF/services/javax.xml.stream.XMLEventFactory\\E" + }, + { + "pattern": "\\QMETA-INF/services/org.flywaydb.core.extensibility.Plugin\\E" + }, + { + "pattern": "\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, + { + "pattern": "\\QMETA-INF/spring-autoconfigure-metadata.properties\\E" + }, + { + "pattern": "\\QMETA-INF/spring-configuration-metadata.json\\E" + }, + { + "pattern": "\\QMETA-INF/spring.factories\\E" + }, + { + "pattern": "\\QMETA-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports\\E" + }, + { + "pattern": "\\QMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports\\E" + }, + { + "pattern": "\\QMETA-INF/MANIFEST.MF\\E" + }, + { + "pattern": "\\Qapplication.properties\\E" + }, + { + "pattern": "\\Qbanner.txt\\E" + }, + { + "pattern": "\\Qcacerts\\E" + }, + { + "pattern": "\\Qch/qos/logback/classic/encoder/PatternLayoutEncoder.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/classic/pattern/ClassicConverter.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/encoder/Encoder.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/encoder/EncoderBase.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/encoder/LayoutWrappingEncoder.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/filter/Filter.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/pattern/CompositeConverter.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/pattern/Converter.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/pattern/DynamicConverter.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/pattern/FormattingConverter.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/pattern/PatternLayoutEncoderBase.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/spi/ContextAware.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/spi/ContextAwareBase.class\\E" + }, + { + "pattern": "\\Qch/qos/logback/core/spi/LifeCycle.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/core/Versioned.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/core/type/TypeReference.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/AnnotationIntrospector.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/JsonDeserializer.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/JsonSerializer.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/Module.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/deser/NullValueProvider.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/deser/ValueInstantiator$Gettable.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/deser/std/StdDeserializer.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/deser/std/StdScalarDeserializer.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/deser/std/StringDeserializer.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/jsonFormatVisitors/JsonFormatVisitable.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/jsonschema/SchemaAware.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/ser/std/StdScalarSerializer.class\\E" + }, + { + "pattern": "\\Qcom/fasterxml/jackson/databind/ser/std/StdSerializer.class\\E" + }, + { + "pattern": "\\Qcom/google/common/cache/CacheLoader.class\\E" + }, + { + "pattern": "\\Qcom/uwetrottmann/tmdb2/Tmdb.class\\E" + }, + { + "pattern": "\\Qconfig/logback.xml\\E" + }, + { + "pattern":"\\Qconfig/baseConfig.yml\\E" + }, + { + "pattern": "\\Qdev/failsafe/event/EventListener.class\\E" + }, + { + "pattern": "\\Qdev/failsafe/function/CheckedSupplier.class\\E" + }, + { + "pattern": "\\Qjakarta/persistence/AttributeConverter.class\\E" + }, + { + "pattern": "\\Qjakarta/persistence/Converter.class\\E" + }, + { + "pattern": "\\Qjakarta/servlet/Filter.class\\E" + }, + { + "pattern": "\\Qjakarta/xml/bind/annotation/adapters/XmlAdapter.class\\E" + }, + { + "pattern": "\\Qmigration/V1__INITIAL.sql\\E" + }, + { + "pattern": "\\Qmigration/V2__SEQUENCES.SQL\\E" + }, + { + "pattern": "\\Qmigration\\E" + }, + { + "pattern": "\\Qmozilla/public-suffix-list.txt\\E" + }, + { + "pattern": "\\Qnzbhydra.png\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/Contained.class\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/JmxEnabled.class\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/Lifecycle.class\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/Valve.class\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/core/RestrictedFilters.properties\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/core/RestrictedListeners.properties\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/core/RestrictedServlets.properties\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/loader/JdbcLeakPrevention.class\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/util/CharsetMapperDefault.properties\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/util/LifecycleBase.class\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/util/LifecycleMBeanBase.class\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/util/ServerInfo.properties\\E" + }, + { + "pattern": "\\Qorg/apache/catalina/valves/ValveBase.class\\E" + }, + { + "pattern": "\\Qorg/flywaydb/core/internal/version.txt\\E" + }, + { + "pattern": "\\Qorg/h2/util/data.zip\\E" + }, + { + "pattern": "\\Qorg/hibernate/boot/model/relational/ExportableProducer.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/dialect/Dialect.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/dialect/H2Dialect.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/dialect/sequence/ANSISequenceSupport.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/dialect/sequence/SequenceSupport.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/exception/spi/ConversionContext.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/hibernate-configuration-3.0.dtd\\E" + }, + { + "pattern": "\\Qorg/hibernate/hibernate-mapping-3.0.dtd\\E" + }, + { + "pattern": "\\Qorg/hibernate/id/BulkInsertionCapableIdentifierGenerator.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/id/Configurable.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/id/IdentifierGenerator.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/id/OptimizableGenerator.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/id/PersistentIdentifierGenerator.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/id/enhanced/SequenceStyleGenerator.class\\E" + }, + { + "pattern": "\\Qorg/hibernate/id/factory/spi/StandardGenerator.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/DevEndpoint.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/InstanceCounter.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/NzbHydraException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/NzbHydraNative.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/NzbHydraNativeEntrypoint.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/CapsGenerator.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/CategoryConverter.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/ExternalApi$CacheEntryValue.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/ExternalApi.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/ExternalApiException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/IllegalAccessException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/MockSearch.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/NewznabJsonTransformer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/NewznabXmlTransformer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/api/stats/ExternalApiStats.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/AsyncSupportFilter.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/AuthAndAccessEventHandler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/AuthWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/HydraAnonymousAuthenticationFilter.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/HydraEmbeddedServletContainer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/HydraGlobalMethodSecurityConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/HydraUserDetailsManager.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/LoginAndAccessAttemptService.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/PersistentLoginsEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/SecurityConfig.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/auth/UserInfosProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/backup/BackupAndRestore.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/backup/BackupTask.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/backup/BackupWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/BaseConfig.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/ConfigProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/ConfigSpringConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/ConfigWeb$ApiHelpResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/ConfigWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/FileSystemBrowser$DirectoryListingRequest.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/FileSystemBrowser$FileSystemEntry.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/FileSystemBrowser$FileSystemSubEntry.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/FileSystemBrowser.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/ValidatingConfig$ConfigValidationResult.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/ValidatingConfig.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/category/CategoriesConfig.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/migration/ConfigMigrationStep.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/config/migration/\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosProvider$DiffableCategoriesConfig.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosProvider$ThreadCpuUsage.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosProvider$TimeAndThreadCpuUsages.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosWeb$Endpoint.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosWeb$PrefixAndEndpoint.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosWeb$ThreadCpuUsageChartData.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosWeb$TimeAndValue.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/debuginfos/DebugInfosWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/DownloadStatusUpdater.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/FileDownloadEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/FileDownloadRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/FileHandler$NzbsDownload.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/FileHandler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/IndexerSpecificDownloadExceptions.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/IndexerUniquenessScoreSaver.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/Downloader$StatusCheckType.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/Downloader.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/DownloaderProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/DownloaderStatusRetrieval.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/DownloaderWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/DownloaderWebSocket.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/nzbget/NzbGet.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/downloaders/sabnzbd/Sabnzbd.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/exceptions/DownloaderException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/nzbs/NzbHandlingWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/torrents/TorrentFileHandler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/downloading/torrents/TorrentHandlingWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/externaltools/ExternalTools$BackendType.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/externaltools/ExternalTools$XdarrAddRequestField.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/externaltools/ExternalTools$XdarrAddRequestResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/externaltools/ExternalTools$XdarrIndexer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/externaltools/ExternalTools.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/externaltools/ExternalToolsWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/fortests/DebugWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/genericstorage/GenericStorage.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/genericstorage/GenericStorageWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/historystats/History$IndexerSearchTO.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/historystats/History$SearchDetails.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/historystats/History.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/historystats/HistoryWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/historystats/Stats.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/historystats/StatsWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Anizb$NewznabHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Anizb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Binsearch$NewznabHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Binsearch.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/DevIndexer$DevIndexerHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/DevIndexer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/DogNzb$NewznabHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/DogNzb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Indexer$BackendType.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Indexer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerApiAccessEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerApiAccessEntityShort.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerApiAccessEntityShortRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerApiAccessRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerSearchEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerSearchRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerStatusesCleanupTask.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerWebAccess$HydraUnmarshallingFailureException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/IndexerWebAccess.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Newznab$NewznabHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/Newznab.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/NzbGeek$NewznabHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/NzbGeek.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/NzbIndex$NewznabHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/NzbIndex.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/QueryGenerator$QueryFormat.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/QueryGenerator.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerChecker$CapsCheckLimit.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerChecker$CheckCapsRequest.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerChecker$CheckerEvent.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerChecker$ConnectionCheckResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerChecker$SingleCheckCapsResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerChecker.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerWeb$JacketConfigReadRequest.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerWeb$JacketConfigReadResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/IndexerWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/JacketConfigRetriever.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/capscheck/SimpleConnectionChecker.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/exceptions/IndexerAccessException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/exceptions/IndexerSearchAbortedException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/status/IndexerLimit.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/status/IndexerLimitRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/status/IndexerStatusesAndLimits$IndexerStatus.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/status/IndexerStatusesAndLimits$LimitsRetrieval.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/status/IndexerStatusesAndLimits.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/status/IndexerStatusesWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/torznab/Torznab$NewznabHandlingStrategy.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/indexers/torznab/Torznab.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/logging/EceptionFilter.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/logging/LogAnonymizer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/logging/LogContentProvider$JsonLogResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/logging/LogContentProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/logging/LoggingMarkerFilter.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mapping/newznab/NewznabResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mapping/newznab/xml/Xml.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mapping/newznab/xml/package-info.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/CustomTmdb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/InfoProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/InfoProviderException.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/MediaInfoWeb$AutocompleteType.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/MediaInfoWeb$CacheKey.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/MediaInfoWeb$MediaInfoTO.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/MediaInfoWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/MovieInfo.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/MovieInfoRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TmdbHandler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TvInfo.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TvInfoRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TvMazeHandler$TvmazeExternals.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TvMazeHandler$TvmazeImage.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TvMazeHandler$TvmazeShow.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TvMazeHandler$TvmazeShowSearch.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/mediainfo/TvMazeHandler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/misc/BrowserOpener.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/misc/DelegatingSSLSocketFactory.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/misc/OpenPortChecker.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/misc/UserAgentMapper.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/misc/WebHooks.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/news/NewsProvider$NewsEntry.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/news/NewsProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/news/NewsWeb$NewsEntryForWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/news/NewsWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/news/ShownNews.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/news/ShownNewsRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/notifications/NotificationEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/notifications/NotificationEvent.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/notifications/NotificationHandler$AppriseMessage.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/notifications/NotificationHandler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/notifications/NotificationRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/notifications/NotificationsWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/DeleteOldDatabaseBackupDetector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/OpenPortProblemDetector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/OutOfMemoryDetector$State.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/OutOfMemoryDetector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/OutdatedWrapperDetector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/ProblemDetector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/ProblemDetectorTask.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/VipExpiryDetector$State.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/VipExpiryDetector$VipExpiryData.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/VipExpiryDetector$VipExpiryDataEntry.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/problemdetection/VipExpiryDetector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/CategoryProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/CustomQueryAndTitleMapping$AffectedValue.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/CustomQueryAndTitleMapping$Mapping.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/CustomQueryAndTitleMapping$MetaData.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/CustomQueryAndTitleMapping$TestRequest.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/CustomQueryAndTitleMapping$TestResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/CustomQueryAndTitleMapping.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/DuplicateDetector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/IndexerForSearchSelector$IndexerForSearchSelection.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/IndexerForSearchSelector.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/InternalSearchResultProcessor.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/SearchModuleConfigProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/SearchModuleProvider.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/SearchResultAcceptor$AcceptorResult.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/SearchResultAcceptor.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/SearchWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/Searcher$SearchEvent.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/Searcher.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/cleanup/HistoryCleanupTask$ASC_DESC.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/cleanup/HistoryCleanupTask.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/cleanup/OldResultsCleanupTask.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/cleanup/ShortIndexerApiAccessCleanup.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/db/IdentifierKeyValuePair.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/db/SearchEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/db/SearchRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/db/SearchResultEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/db/SearchResultRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/searchrequests/SearchRequestFactory.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/uniqueness/IndexerUniquenessScoreEntity.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/searching/uniqueness/IndexerUniquenessScoreEntityRepository.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/springconfig/AppConfig.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/springconfig/ControllerAdvices.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/springconfig/GracefulSpringShutdown$GracefulShutdown.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/springconfig/GracefulSpringShutdown.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/systemcontrol/SystemControl.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/systemcontrol/SystemControlWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/tasks/HydraTaskConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/tasks/HydraTaskScheduler$TaskInformation.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/tasks/HydraTaskScheduler$TaskRuntimeInformation.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/tasks/HydraTaskScheduler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/tasks/HydraTasksWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/AutomaticUpdater.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/UpdateManager$BlockedVersion.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/UpdateManager$PackageInfo.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/UpdateManager$UpdateEvent.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/UpdateManager$UpdateInfo.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/UpdateManager.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/UpdatesWeb$VersionsInfo.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/update/UpdatesWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/ErrorHandler$JsonExceptionResponse.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/ErrorHandler.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/HelpWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/Interceptor.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/MainWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/NzbDetailsWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/UrlCalculator.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/WebConfiguration$NewznabAndTorznabResponseNamespaceFixer.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/WebConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/WebSocketConfig.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/web/WelcomeWeb.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/AbstractBufferingClientHttpRequest.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactory$SockProxySocketFactory.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactory.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/Ssl$AllTrustingManager.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/Ssl$KeyAndTrustManagers.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/Ssl$SniWhitelistingSocketFactory.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/Ssl$SslVerificationState.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/Ssl.class\\E" + }, + { + "pattern": "\\Qorg/nzbhydra/webaccess/WebAccess.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/Aware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/BeanClassLoaderAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/BeanFactoryAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/BeanNameAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/DisposableBean.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/InitializingBean.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/SmartInitializingSingleton.class\\E" + }, + { + "pattern": "\\Qorg/springframework/beans/factory/config/BeanPostProcessor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/audit/AuditEventsEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/availability/AvailabilityHealthContributorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/beans/BeansEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/condition/ConditionsReportEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/context/ShutdownEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration$JerseyServletEndpointManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration$WebEndpointServletConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration$EndpointObjectMapperWebMvcConfigurer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/flyway/FlywayEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/AbstractCompositeHealthContributorConfiguration$ReflectionIndicatorFactory.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/AbstractCompositeHealthContributorConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/ConditionalOnEnabledHealthIndicator.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthContributorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration$AdaptedReactiveHealthContributors.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration$HealthEndpointGroupsBeanPostProcessor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration$JerseyAdditionalHealthEndpointPathsConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration$JerseyAdditionalHealthEndpointPathsResourcesRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration$MvcAdditionalHealthEndpointPathsConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/info/InfoEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfiguration$RoutingDataSourceHealthContributor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfiguration$LogFileCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration$OnEnabledLoggingSystemCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/mail/MailHealthContributorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/management/HeapDumpWebEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/management/ThreadDumpEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration$MultipleNonPrimaryMeterRegistriesCondition$NoMeterRegistryCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration$MultipleNonPrimaryMeterRegistriesCondition$SingleInjectableMeterRegistry.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration$MultipleNonPrimaryMeterRegistriesCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/LogbackMetricsAutoConfiguration$LogbackLoggingCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/LogbackMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/MetricsEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/NoOpMeterRegistryConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration$Cache2kCacheMeterBinderProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration$CaffeineCacheMeterBinderProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration$HazelcastCacheMeterBinderProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration$JCacheCacheMeterBinderProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration$RedisCacheMeterBinderProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsRegistrarConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/export/ConditionalOnEnabledMetricsExport.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/integration/IntegrationMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration$DataSourcePoolMetadataMeterBinder.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration$HikariDataSourceMeterBinder.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsListenerAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$MeterObservationHandlerConfiguration$OnlyMetricsMeterObservationHandlerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$MeterObservationHandlerConfiguration$TracingAndMetricsObservationHandlerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$MeterObservationHandlerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$MetricsWithTracingConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$OnlyMetricsConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$OnlyTracingConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration$MeterFilterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration$MeterFilterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration$JerseyRequestMatcherConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration$MvcRequestMatcherConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthContributorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/ManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration$ReactiveHttpExchangesConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration$ServletHttpExchangesConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/exchanges/HttpExchangesEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration$ReactiveWebConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration$ServletWebConfiguration$SpringMvcConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration$ServletWebConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/server/ConditionalOnManagementPort.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/server/EnableManagementContext.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration$DifferentManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration$SameManagementContextConfiguration$EnableSameManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration$SameManagementContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/server/ManagementContextConfigurationImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementContextAutoConfiguration$ApplicationContextFilterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementContextAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/AutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/AutoConfigurationImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/AutoConfigurationPackage.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/AutoConfigurationPackages$Registrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/AutoConfigureAfter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/AutoConfigureBefore.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/AutoConfigureOrder.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/EnableAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnBean.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnClass.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnMissingClass.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnProperty.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnResource.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/condition/ConditionalOnWebApplication.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/context/LifecycleAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/AbstractRepositoryConfigurationSourceSupport.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration$BootstrapExecutorCondition$DeferredBootstrapMode.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration$BootstrapExecutorCondition$LazyBootstrapMode.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration$BootstrapExecutorCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration$JpaRepositoriesImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/data/web/SpringDataWebAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayAutoConfigurationRuntimeHints.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayDataSourceCondition$DataSourceBeanCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayDataSourceCondition$FlywayUrlCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayDataSourceCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$LocationResolver.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$StringOrNumberToMigrationVersionConverter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/gson/GsonAutoConfiguration$StandardGsonBuilderCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration$GsonHttpMessageConverterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration$JacksonAndJsonbUnavailableCondition$JacksonAvailable.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration$JacksonAndJsonbUnavailableCondition$JsonbPreferred.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration$JacksonAndJsonbUnavailableCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration$PreferGsonOrJacksonAndJsonbUnavailableCondition$GsonPreferred.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration$PreferGsonOrJacksonAndJsonbUnavailableCondition$JacksonJsonbUnavailable.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration$PreferGsonOrJacksonAndJsonbUnavailableCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/GsonHttpMessageConvertersConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration$HttpMessageConvertersAutoConfigurationRuntimeHints.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration$NotReactiveWebApplicationCondition$ReactiveWebApplication.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration$NotReactiveWebApplicationCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/JacksonHttpMessageConvertersConfiguration$MappingJackson2XmlHttpMessageConverterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/JacksonHttpMessageConvertersConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/http/JsonbHttpMessageConvertersConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration$GitResourceAvailableCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonAutoConfigurationRuntimeHints.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonMixinConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonObjectMapperConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$ParameterNamesModuleConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$EmbeddedDatabaseCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$EmbeddedDatabaseConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$PooledDataSourceAvailableCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$PooledDataSourceCondition$ExplicitType.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$PooledDataSourceCondition$PooledDataSourceAvailable.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$PooledDataSourceCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$PooledDataSourceConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Dbcp2.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Generic.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$OracleUcp.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Tomcat.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration$JdbcTransactionManagerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/JdbcTemplateConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/NamedParameterJdbcTemplateConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration$CommonsDbcp2PoolDataSourceMetadataProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration$OracleUcpPoolDataSourceMetadataProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration$TomcatDataSourcePoolMetadataProviderConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/jmx/JmxAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/mail/MailSenderValidatorAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration$NamingStrategiesHibernatePropertiesCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration$JpaWebConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration$PersistenceManagedTypesConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/ConditionalOnDefaultWebSecurity.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/DefaultWebSecurityCondition$Beans.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/DefaultWebSecurityCondition$Classes.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/DefaultWebSecurityCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/SecurityDataConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration$ReactiveUserDetailsServiceCondition$RSocketSecurityEnabledCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration$ReactiveUserDetailsServiceCondition$ReactiveWebApplicationCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration$ReactiveUserDetailsServiceCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration$SecurityFilterChainConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration$WebSecurityEnablerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/sql/init/R2dbcInitializationConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfiguration$SqlInitializationModeCondition$ModeIsNever.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfiguration$SqlInitializationModeCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/TemplateEngineConfigurations$DefaultTemplateEngineConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/TemplateEngineConfigurations$ReactiveTemplateEngineConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$DataAttributeDialectConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$DefaultTemplateResolverConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$ThymeleafSecurityDialectConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$ThymeleafWebFluxConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$ThymeleafWebLayoutConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$ThymeleafWebMvcConfiguration$ThymeleafViewResolverConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration$ThymeleafWebMvcConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration$EnableTransactionManagementConfiguration$CglibAutoProxyConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration$EnableTransactionManagementConfiguration$JdkDynamicAutoProxyConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration$EnableTransactionManagementConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration$TransactionTemplateConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration$NotReactiveWebApplicationCondition$ReactiveWebApplication.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration$NotReactiveWebApplicationCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration$JettyWebServerFactoryCustomizerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration$NettyWebServerFactoryCustomizerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration$UndertowWebServerFactoryCustomizerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DefaultDispatcherServletCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletRegistrationCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration$LocaleCharsetMappingsCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration$BeanPostProcessorsRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration$ForwardedHeaderFilterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration$ForwardedHeaderFilterCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration$EmbeddedJetty.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration$EmbeddedTomcat.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration$EmbeddedUndertow.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$OptionalPathExtensionContentNegotiationStrategy.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$ProblemDetailsErrorHandlingConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$ResourceChainCustomizerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$ResourceChainResourceHandlerRegistrationCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$ResourceHandlerRegistrationCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$ErrorPageCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$ErrorTemplateMissingCondition.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$PreserveErrorControllerTargetClassPostProcessor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$StaticView.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration$WebSocketMessageConverterConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration$JettyWebSocketConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration$UndertowWebSocketConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/context/annotation/DeterminableImports.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/context/properties/ConfigurationProperties.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/context/properties/EnableConfigurationProperties.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/diagnostics/AbstractFailureAnalyzer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/diagnostics/FailureAnalyzer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/logging/logback/ColorConverter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/sql/init/dependency/DatabaseInitializationDependencyConfigurer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/web/embedded/tomcat/TomcatConnectorCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/web/server/WebServerFactoryCustomizer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/boot/web/server/mime-mappings.properties\\E" + }, + { + "pattern": "\\Qorg/springframework/cache/annotation/AbstractCachingConfiguration$CachingConfigurerSupplier.class\\E" + }, + { + "pattern": "\\Qorg/springframework/cache/annotation/AbstractCachingConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/cache/annotation/CachingConfigurationSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/cache/annotation/EnableCaching.class\\E" + }, + { + "pattern": "\\Qorg/springframework/cache/annotation/ProxyCachingConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/ApplicationContextAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/ApplicationContextInitializer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/ApplicationEvent.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/ApplicationListener.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/EnvironmentAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/ResourceLoaderAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/AdviceModeImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/AutoProxyRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/ComponentScan.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/Conditional.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/Configuration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/DeferredImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/Import.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/ImportAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/ImportBeanDefinitionRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/ImportRuntimeHints.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/Primary.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/Role.class\\E" + }, + { + "pattern": "\\Qorg/springframework/context/annotation/Scope.class\\E" + }, + { + "pattern": "\\Qorg/springframework/core/ParameterizedTypeReference.class\\E" + }, + { + "pattern": "\\Qorg/springframework/core/annotation/Order.class\\E" + }, + { + "pattern": "\\Qorg/springframework/core/convert/converter/Converter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/core/env/EnvironmentCapable.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/jpa/repository/JpaRepository.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/jpa/util/JpaMetamodelCacheCleanup.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/repository/CrudRepository.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/repository/ListCrudRepository.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/EnableSpringDataWebSupport$QuerydslActivator.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/EnableSpringDataWebSupport$SpringDataWebConfigurationImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/EnableSpringDataWebSupport.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/ProjectingArgumentResolverRegistrar$ProjectingArgumentResolverBeanPostProcessor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/ProjectingArgumentResolverRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/SpringDataJacksonConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/SpringDataJacksonModules.class\\E" + }, + { + "pattern": "\\Qorg/springframework/data/web/config/SpringDataWebConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/HttpInputMessage.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/HttpMessage.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/HttpOutputMessage.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/HttpRequest.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/client/AbstractClientHttpRequest.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/client/AbstractClientHttpResponse.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/client/ClientHttpRequest.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/client/ClientHttpRequestFactory.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/client/ClientHttpRequestInterceptor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/client/ClientHttpResponse.class\\E" + }, + { + "pattern": "\\Qorg/springframework/http/converter/HttpMessageConverter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/scheduling/Trigger.class\\E" + }, + { + "pattern": "\\Qorg/springframework/scheduling/annotation/AbstractAsyncConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/scheduling/annotation/AsyncConfigurationSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/scheduling/annotation/EnableAsync.class\\E" + }, + { + "pattern": "\\Qorg/springframework/scheduling/annotation/EnableScheduling.class\\E" + }, + { + "pattern": "\\Qorg/springframework/scheduling/annotation/ProxyAsyncConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/scheduling/annotation/SchedulingConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/authentication/AuthenticationDetailsSource.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration$AuthenticationManagerDelegator.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration$LazyPasswordEncoder.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/authentication/configuration/EnableGlobalAuthentication.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/method/configuration/GlobalMethodSecuritySelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/method/configuration/MethodSecurityMetadataSourceAdvisorRegistrar.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/EnableWebSecurity.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration$LazyPasswordEncoder.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration$AnnotationAwareOrderComparator.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/core/userdetails/UserDetailsService.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/provisioning/UserDetailsManager.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/web/access/AccessDeniedHandler.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/web/access/AccessDeniedHandlerImpl.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/web/authentication/AnonymousAuthenticationFilter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/web/authentication/WebAuthenticationDetails.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/web/authentication/WebAuthenticationDetailsSource.class\\E" + }, + { + "pattern": "\\Qorg/springframework/security/web/authentication/www/BasicAuthenticationFilter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/transaction/annotation/AbstractTransactionManagementConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/transaction/annotation/EnableTransactionManagement.class\\E" + }, + { + "pattern": "\\Qorg/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/transaction/annotation/TransactionManagementConfigurationSelector.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/bind/annotation/ControllerAdvice.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/bind/annotation/ResponseBody.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/bind/annotation/RestController.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/context/ServletContextAware.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/context/annotation/RequestScope.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/filter/GenericFilterBean$FilterConfigPropertyValues.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/filter/GenericFilterBean.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/filter/OncePerRequestFilter.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/servlet/HandlerInterceptor.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport$NoOpValidator.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/servlet/config/annotation/WebMvcConfigurer.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/socket/config/annotation/DelegatingWebSocketMessageBrokerConfiguration.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/socket/config/annotation/EnableWebSocketMessageBroker.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.class\\E" + }, + { + "pattern": "\\Qorg/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurer.class\\E" + }, + { + "pattern": "\\Qorg/thymeleaf/thymeleaf.properties\\E" + }, + { + "pattern": "\\Qstatic/css/alllibs.css\\E" + }, + { + "pattern": "\\Qstatic/css/grey.css\\E" + }, + { + "pattern": "\\Qstatic/fonts/fontawesome-webfont.woff2\\E" + }, + { + "pattern": "\\Qstatic/fonts/glyphicons-halflings-regular.woff2\\E" + }, + { + "pattern": "\\Qstatic/img/banner-grey-dark.png\\E" + }, + { + "pattern": "\\Qstatic/img/favicon.ico\\E" + }, + { + "pattern": "\\Qstatic/img/favicon180.png\\E" + }, + { + "pattern": "\\Qstatic/img/spinner.gif\\E" + }, + { + "pattern": "\\Qstatic/js/alllibs.js\\E" + }, + { + "pattern": "\\Qstatic/js/nzbhydra.js\\E" + }, + { + "pattern": "\\Qstatic/js/templates.js\\E" + }, + { + "pattern": "\\Qtemplates/\\E" + }, + { + "pattern": "\\Qtemplates/error.html\\E" + }, + { + "pattern": "\\Qtemplates/index.html\\E" + }, + { + "pattern": "\\QwrapperHashes2.json\\E" + }, + { + "pattern": "java.base:\\Qjdk/internal/icu/impl/data/icudt67b/nfkc.nrm\\E" + }, + { + "pattern": "java.base:\\Qjdk/internal/icu/impl/data/icudt67b/uprops.icu\\E" + }, + { + "pattern": "java.base:\\Qsun/net/idn/uidna.spp\\E" + }, + { + "pattern": "java.base:\\Qsun/net/www/content-types.properties\\E" + }, + { + "pattern": "java.base:\\Qsun/text/resources/SentenceBreakIteratorData\\E" + } + ]}, + "bundles": [ + { + "name": "jakarta.servlet.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "jakarta.servlet.http.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.authenticator.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.authenticator.jaspic.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.connector.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.core.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.deploy.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.loader.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.mapper.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.mbeans.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.realm.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.security.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.session.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.startup.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.util.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.valves.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.catalina.webresources.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.coyote.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.coyote.http11.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.coyote.http11.filters.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.naming.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.buf.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.codec.binary.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.compat.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.descriptor.web.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.http.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.http.parser.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.modeler.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.net.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.scan.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.security.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.util.threads.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.websocket.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "org.apache.tomcat.websocket.server.LocalStrings", + "locales": [ + "" + ] + }, + { + "name": "joptsimple.ExceptionMessages", + "locales": [ + "en" + ] + }, + { + "name": "org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.Messages", + "locales": [ + "en" + ] + } + ] +} diff --git a/core/src/main/resources/META-INF/native-image/serialization-config.json b/core/src/main/resources/META-INF/native-image/serialization-config.json new file mode 100644 index 000000000..27abafe5f --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/serialization-config.json @@ -0,0 +1,12 @@ +{ + "types":[ + { + "name":"org.nzbhydra.searching.IndexerForSearchSelector$$SpringCGLIB$$0", + "customTargetConstructorClass":"java.lang.Object" + } + ], + "lambdaCapturingTypes":[ + ], + "proxies":[ + ] +} diff --git a/core/src/main/resources/META-INF/spring.factories b/core/src/main/resources/META-INF/spring.factories index 317f1f0df..e0fdaae0a 100644 --- a/core/src/main/resources/META-INF/spring.factories +++ b/core/src/main/resources/META-INF/spring.factories @@ -1 +1,3 @@ -org.springframework.boot.diagnostics.FailureAnalyzer=org.nzbhydra.exceptionanalyzers.YAMLExceptionAnalyzer \ No newline at end of file +org.springframework.boot.diagnostics.FailureAnalyzer=org.nzbhydra.exceptionanalyzers.YAMLExceptionAnalyzer +org.springframework.aot.hint.RuntimeHintsRegistrar=org.nzbhydra.NativeHints +org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=org.nzbhydra.database.DatabaseRecreationConfig diff --git a/core/src/main/resources/changelog.json b/core/src/main/resources/changelog.json index 93c57229d..ca1072c0a 100644 --- a/core/src/main/resources/changelog.json +++ b/core/src/main/resources/changelog.json @@ -1,50 +1,89 @@ -[ { - "version" : "v4.7.6", - "date" : "2022-12-22", - "changes" : [ { - "type" : "feature", - "text" : "The next major update (5.0.0) will require a manual update. I've added logic to handle this and prevent the automatic updates from being executed." - } ], - "final" : true -}, { - "version" : "v4.7.5", - "date" : null, - "changes" : [ { - "type" : "fix", - "text" : "Configure separate indexers in lidarr using categories. See #802" - } ], - "final" : true -}, { - "version" : "v4.7.4", - "date" : "2022-12-14", - "changes" : [ { - "type" : "fix", - "text" : "Hopefully make the java update message disappear after a java update. See #810" - } ], - "final" : true -}, { - "version" : "v4.7.3", - "date" : "2022-12-06", - "changes" : [ { - "type" : "note", - "text" : "A future update will require Java 17. To prepare for that a message will be shown asking you to update your system accordingly. If you're running NZBHydra2 in docker you don't need to do anything." - }, { - "type" : "feature", - "text" : "Set the environment variable NZBHYDRA_DISABLE_UPDATE to true to disable the NZBHydra update mechanism (similar as to when it's run inside docker). This can be used by package maintainers. See #809" - } ], - "final" : true -}, { - "version" : "v4.7.2", - "date" : "2022-11-30", - "changes" : [ { - "type" : "fix", - "text" : "Handle results without date." - }, { - "type" : "fix", - "text" : "Fix typo in apprise notification handler." - }, { - "type" : "fix", - "text" : "Hopefully fix notification sending test on arch-nzbhydra2. See #806" +[ + { + "version": "v5.0.0", + "changes": [ + { + "type": "feature", + "text": "Massive upgrade of the underlying framework and used libraries. Java 17 is now mandatory." + }, + { + "type": "feature", + "text": "Update of database to a newer version. This requires a recreation of the whole database which hopefully will be executed automatically and without errors ;-)" + }, + { + "type": "feature", + "text": "Ensure integrity of database when creating backup." + }, + { + "type": "feature", + "text": "Only delete old backups if a newer one exists." + }, + { + "type": "feature", + "text": "Improve startup time by moving some tasks into the background." + }, + { + "type": "fix", + "text": "Improve startup time by moving some tasks into the background." + }, + { + "type": "fix", + "text": "Misleading error messages were shown when shutting down." + } + ], + "final": false + }, + { + "version": "v4.7.5", + "changes": [ + { + "type": "fix", + "text": "Configure separate indexers in lidarr using categories. See #802" + } + ], + "final": true + }, + { + "version": "v4.7.4", + "date": "2022-12-14", + "changes": [ + { + "type": "fix", + "text": "Hopefully make the java update message disappear after a java update. See #810" + } + ], + "final": true + }, + { + "version": "v4.7.3", + "date": "2022-12-06", + "changes": [ + { + "type": "note", + "text": "A future update will require Java 17. To prepare for that a message will be shown asking you to update your system accordingly. If you're running NZBHydra2 in docker you don't need to do anything." + }, + { + "type": "feature", + "text": "Set the environment variable NZBHYDRA_DISABLE_UPDATE to true to disable the NZBHydra update mechanism (similar as to when it's run inside docker). This can be used by package maintainers. See #809" + } + ], + "final": true + }, + { + "version": "v4.7.2", + "date": "2022-11-30", + "changes": [ + { + "type": "fix", + "text": "Handle results without date." + }, + { + "type": "fix", + "text": "Fix typo in apprise notification handler." + }, + { + "type": "fix", + "text": "Hopefully fix notification sending test on arch-nzbhydra2. See #806" }, { "type" : "fix", "text" : "Fix automatic configuration of Lidarr. See #802" @@ -3642,4 +3681,4 @@ "text" : "First public release. Welcome!" } ], "final" : true -} ] \ No newline at end of file +} ] diff --git a/core/src/main/resources/changelog.yaml b/core/src/main/resources/changelog.yaml new file mode 100644 index 000000000..9d99a75d6 --- /dev/null +++ b/core/src/main/resources/changelog.yaml @@ -0,0 +1,3243 @@ +--- +- version: v5.0.0 + changes: + - type: feature + text: Massive upgrade of the underlying framework and used libraries. Java is not needed anymore in most cases. + Highly increased startup and memory performance (on my machine using docker and as a fresh install it starts + in 0.9 seconds and uses 180MB memory now versus 9 seconds and 332MB memory before). This is the result of weeks + of work and testing. I hope everything goes as smooth as possible. + - type: feature + text: Update of database to a newer version. This requires a recreation of the + whole database which hopefully will be executed automatically and without errors + ;-) + - type: feature + text: Improve startup time by moving some tasks into the background. + - type: feature + text: Improve performance when handling search results and making HTTP calls. + - type: feature + text: Ensure integrity of database when creating backup. + - type: feature + text: Only delete old backups if a newer one exists. + - type: fix + text: Prevent some misleading error messages that were shown when shutting down. + final: false +- version: v4.7.5 + changes: + - type: fix + text: 'Configure separate indexers in lidarr using categories. See #802' + final: true +- version: v4.7.4 + date: '2022-12-14' + changes: + - type: fix + text: 'Hopefully make the java update message disappear after a java update. See + #810' + final: true +- version: v4.7.3 + date: '2022-12-06' + changes: + - type: note + text: A future update will require Java 17. To prepare for that a message will + be shown asking you to update your system accordingly. If you're running NZBHydra2 + in docker you don't need to do anything. + - type: feature + text: 'Set the environment variable NZBHYDRA_DISABLE_UPDATE to true to disable + the NZBHydra update mechanism (similar as to when it''s run inside docker). + This can be used by package maintainers. See #809' + final: true +- version: v4.7.2 + date: '2022-11-30' + changes: + - type: fix + text: Handle results without date. + - type: fix + text: Fix typo in apprise notification handler. + - type: fix + text: 'Hopefully fix notification sending test on arch-nzbhydra2. See #806' + - type: fix + text: 'Fix automatic configuration of Lidarr. See #802' + - type: note + text: As you can see development has slowed down a bit. The reason is kind of + a mix of burnout, a new job and some other stuff. I'll try to get most bugs + fixed faster and get back to some new features next year. + final: true +- version: v4.7.1 + date: '2022-09-18' + changes: + - type: feature + text: Improve display of errors on startup. + - type: fix + text: Properly handle errors that occur during the detection of open ports. + final: false +- version: v4.7.0 + date: '2022-09-18' + changes: + - type: feature + text: 'Use custom mappings to transform indexer result titles. Use this to clean + up titles, add season or episode to it or whatever. See #794' + - type: fix + text: Some of you have an instance running which is exposed to the internet, without + any authentication method. I previously tried to recognize this by some heuristic + which was a bit naive and caused a lot of false positives. NZBHydra will now + periodically try to determine your public IP and actually check if the used + port is open. This might still not always work (e.g. in when you're running + it using a VPN in which case I guess you know what're doing. Ultimately it's + up to you to get your shit together. + - type: fix + text: Only warn about settings violating indexers' rules if the indexers are actually + enabled. + - type: fix + text: Fix saving config with custom mappings. + final: false +- version: v4.6.1 + date: '2022-08-23' + changes: + - type: fix + text: Fix startup error for new instances. Thanks @ cdloh. + final: true +- version: v4.6.0 + date: '2022-08-22' + changes: + - type: feature + text: Add option to replace german umlauts and special characters. + final: true +- version: v4.5.0 + date: '2022-07-09' + changes: + - type: feature + text: 'Automatically use NZB access and adding types required by certain indexers. + See #784.' + - type: feature + text: Add debug logging for category mapping. + final: false +- version: v4.4.0 + date: '2022-06-26' + changes: + - type: feature + text: Add validation to ensure your configuration matches the requirements of + a certain indexer. + - type: feature + text: Warn when exposing NZBHydra to the internet via host 0.0.0.0 with no authentication + enabled. + - type: note + text: In the same vein I decided to remove the option to ignore warnings when + saving the config. You'll just have to live with it or, ideally, fix the things + causing the warnings. + - type: note + text: All the above stems from the fact that a lot of people (=idiots) have their + NZBHydra (or *arr) instances wide open to the world without any authentication + whatsoever. DO NOT DO THAT! People will steal your API keys and possibly get + your indexer access disabled or revoked for good. I'm trying to automatically + detect that but it's not easy distinguishing valid accesses from fraudulent + ones. + final: true +- version: v4.3.3 + date: '2022-06-15' + changes: + - type: fix + text: Fix error when using an HTTP proxy without username / password. + - type: fix + text: 'Use API hit information from indexer request when no download information + was provided. In that case calculate the downloads from the history. See #778' + - type: fix + text: Fix API hit and download detection for DogNZB. + - type: fix + text: Add the current API hit to the number of reported API hits in response. + - type: fix + text: Fix name of logging marker "Custom mapping" (was "Config mapping"). + final: true +- version: v4.3.2 + date: '2022-06-13' + changes: + - type: fix + text: 'Fix use of groups in custom search request mapping. See #700' + - type: fix + text: 'Fix download of backup files. See #772' + - type: note + text: The mysterious issues with connections to indexers failing (and perhaps + some other issues) were caused by changes in the linuxserver.io image and should + be fixed by now. + final: true +- version: v4.3.1 + date: '2022-05-02' + changes: + - type: note + text: I removed the OpenAPI docs as for some really weird reason it may have introduced + some unexpected bugs when connecting to indexers or even when trying to update + the database + final: true +- version: v4.3.0 + date: '2022-04-03' + changes: + - type: feature + text: 'Allow to configure an indexer''s API path. See #766' + - type: feature + text: OpenAPI docs are now available under http://127.0.0.1:5061/v3/api-docs/. + This will only be interesting for very few (if any) users. Unfortunately I couldn't + get the swagger UI working. You'll have to visit http://127.0.0.1:5061/swagger-ui/index.html + and paste the api-docs URL. + final: true +- version: v4.2.1 + date: '2022-03-24' + changes: + - type: note + text: Added a banner of and link to NewsDemon. Thanks for sponsoring me! + - type: fix + text: 'The dismiss button for the banner shown after an automatic update has been + installed didn''t work reliably. See #737' + final: true +- version: v4.2.0 + date: '2022-03-04' + changes: + - type: feature + text: 'Add entry to display options to always show result titles. By default they''re + hidden when grouping results with the same name. See #763' + - type: feature + text: 'Add dismiss button to banner shown after an automatic update has been installed. + See #737' + - type: fix + text: 'Use link to comments as detail link for torznab results. For some indexer + the details would previously go to the download link. See #758' + - type: fix + text: 'Only show video related quick filter buttons when searching in a TV or + movie category. See #732' + - type: note + text: I'm currently testing a new version of the database library. This new version + may hopefully be a bit more performant, may result in smaller database files + (for those suffering from very larg ones) and / or give me options to fine tune + how data is compacted (for those where a lot of IO is produced). Unfortunately + it means that the old database needs to be migrated which is always a bit hairy. + If you're interested in helping me by testing an alpha version please leave + me a note in this + github Issue. + final: true +- version: v4.1.0 + date: '2022-01-30' + changes: + - type: feature + text: 'Allow certain notifications to be filtered (not shown / being sent). See + #761' + - type: fix + text: Change shebang for python 3 wrapper so that it siginifies being a python + 3 script. + final: true +- version: v4.0.2 + date: '2022-01-30' + changes: + - type: fix + text: 'Fix automatic configuration of Sonarr v3. See #753' + final: true +- version: v4.0.1 + date: '2022-01-10' + changes: + - type: fix + text: Properly read X-Forwarded-For header and original IP. + - type: fix + text: Execute connection check after an indexer's API key has changed. + final: true +- version: v4.0.0 + date: '2022-01-02' + changes: + - type: feature + text: Update framework libraries and add support for Java 17. + - type: feature + text: NZBHydra now (hopefully) properly supports indexers that return more than + 100 results per API page. When I started developing 8 years ago no indexer I + knew of returned more than 100 results, now some return 500 or even 1000. That + obviously often tremendously reduces the API hits needed to fill a page or find + a certain result. NZBHydra will now request 1000 results (many indexers will + still only return 100) per page. The page size of the results returned by NZBHydra + is still 100 (if not overwritten in the API request). The amount of API hits + made by local programs doesn't matter and the time and performance overhead + are negligible. Please note that these changes required some hefty changes in + the deeps of the search logic and may have produced some bugs. Let me know if + it works as expected. + - type: feature + text: Show assigned colors of indexers in config list. + - type: note + text: Happy new year! + final: true +- version: v3.18.4 + date: '2021-12-11' + changes: + - type: fix + text: Update logging library to a newer version due to a security issue. This + isn't much of an issue, in my opinion, as I use a different library although + this one is used by others. It also only affects JDKs that are older than a + year and it's not an issue on docker containers. + final: true +- version: v3.18.3 + date: '2021-12-06' + changes: + - type: fix + text: Added nzbgeek to the list of indexers which don't support movie/tvsearch + searches without IDs. + final: true +- version: v3.18.2 + date: '2021-12-06' + changes: + - type: fix + text: Some indexers do not support movie/tvsearch type queries without IDs but + with word based queries (I know of nzbplanet and dognzb). For these indexers + the search type is automatically switched to search when no IDs but a word query + is given. + final: true +- version: v3.18.1 + date: '2021-12-01' + changes: + - type: fix + text: Fix exception when unexpected java version is found. Why the fuck does every + JDK have to have its own version format? + final: true +- version: v3.18.0 + date: '2021-11-27' + changes: + - type: fix + text: Make sure that when the NZBHydra API is accessed with a duplicate /api in + the URL this is not interpreted as wanting to only use an indexer with the name + "api". + - type: feature + text: Abort on startup if incompatible Java version is used. + final: true +- version: v3.17.3 + date: '2021-10-05' + changes: + - type: fix + text: Ugh, don't ask. I'm glad that releases don't cost anything. + final: true +- version: v3.17.2 + date: '2021-10-05' + changes: + - type: fix + text: 'The "What''s new" views were empty. If you want the gritty details: I make + a call to the backend /updates/changesSinceUpTo/3.17.1 which is supposed to + return all changes between the current version and 3.17.1, in this example. + For some stupid reason the backend framework converts the 3.17.1 to 3.17 which + means for the running version 3.17.0 the changes between 3.17 and 3.17.0 were + shown which are obviously empty.' + final: true +- version: v3.17.1 + date: '2021-10-05' + changes: + - type: fix + text: 'Only allow (ZIP) files to be downloaded that were created by NZBHydra. + See #744.' + - type: fix + text: 'Hide button to download results as ZIP if access to indexer results is + configured to work via redirect. See #734.' + final: true +- version: v3.17.0 + date: '2021-10-03' + changes: + - type: feature + text: 'Show beta releases in update section even when beta releases are disabled. + You won''t get notifications and automatic updates will still respect the config + but you can then choose to install a beta version without having to switch to + the beta branch. See #730.' + - type: feature + text: 'Show loading spinner while loading more results. See #729.' + - type: fix + text: 'Fix wrong API path used when configuring Radarr v4. See #731.' + - type: fix + text: Fix display of fixes in version history (the little orange badge shown in + the updates section next to this entry). + - type: fix + text: Don't log output of URL calls when status 429 is returned (Too Many Requests). + final: true +- version: v3.16.2 + date: '2021-09-27' + changes: + - type: fix + text: Fix missing quotation mark in base config (only used for new installations). + final: false +- version: v3.16.1 + date: '2021-09-27' + changes: + - type: fix + text: Roll back release of 3.16.0 because of some problems with form based logins + which need some more analysis. + final: false +- version: v3.16.0 + date: '2021-09-25' + changes: + - type: feature + text: Add support for Java 16. Please not that there's no reason for you to just + willy-nilly update the java major version (e.g. 11 to 16). Newer releases are + not automatically better or safer. Installing patches (e.g. Java 11.0.0 to Java + 11.0.2 or such) is enough. Java 17 is still not supported. + - type: feature + text: 'Add "-xpost" to the list of trailing words to remove. See #717.' + - type: fix + text: 'Make checkboxes and radioboxes grayscale because new browsers show them + in weird blue. See #727.' + final: false +- version: v3.15.2 + date: '2021-08-27' + changes: + - type: fix + text: Remove dereferer.org from preset config and from any instances still using + it. Modern browsers all support the Referrer-Policy header that is set by NZBHydra + anyway. + final: true +- version: v3.15.1 + date: '2021-08-05' + changes: + - type: feature + text: When configuring an external tool like sonarr and forgetting to provide + its URL base in the URL it will return a misleading response. Hydra will now + recognize this case and show a helpful message. + - type: fix + text: Fix sorting of search state messages. + - type: fix + text: Log to browser console which quick filters don't match a result. This will + help with debugging some issues in this area. + final: true +- version: v3.15.0 + date: '2021-07-10' + changes: + - type: feature + text: Debug infos can now be created and directly uploaded to https://ufile.io/ + for easier sharing. + - type: feature + text: The tooltip in the search results for the display of rejected, loaded and + filtered results now also shows the number of filtered results for each reason + (e.g. x results being too small, y results already downloaded). + - type: feature + text: 'The "Searching... please wait" box now highlights indexer searches that + produced results. The messages are also sorted by indexer name and start with + the number of results to allow easier reading. See #696.' + final: true +- version: v3.14.2 + date: '2021-05-23' + changes: + - type: fix + text: 'Min and max size API parameters were ignored. See #705' + final: true +- version: v3.14.1 + date: '2021-04-22' + changes: + - type: feature + text: Added NZB360, Readarr and Mylar to mapped user agents. Thanks to SAS-1 + - type: fix + text: 'With the option to transform newznab categories enabled if provided categories + of an API call could not be mapped to a category they weren''t used at all. + See #704.' + final: true +- version: v3.14.0 + date: '2021-04-11' + changes: + - type: feature + text: 'Custom mapping for queries and titles. This allows you to customize / change + the values used by external tools or returned by metadata providers like TVDB. + See #700.' + final: true +- version: v3.13.2 + date: '2021-03-20' + changes: + - type: fix + text: 'Fix connection check for nzbgeek. See #695.' + - type: note + text: 'Java 16 is not supported. See #697.' + final: true +- version: v3.13.1 + date: '2021-03-10' + changes: + - type: fix + text: 'Fix external configuration of Readarr (0.1.0.520+). See #693.' + final: true +- version: v3.13.0 + date: '2021-02-23' + changes: + - type: feature + text: From now on I'll refer to the appropriate GitHub issues in the changelog + (if I don't forget it). + - type: fix + text: 'Improve category detection for MyAnonaMouse. See #689.' + - type: fix + text: 'Don''t crash GUI when result titles are empty. See #690.' + - type: fix + text: 'Clarify the restrictions section in the auth config. See #687.' + final: true +- version: v3.12.0 + date: '2021-02-13' + changes: + - type: feature + text: 'Add button to send results to black hole from download history. See #685' + - type: feature + text: 'Add support for custom parameters to be sent to indexers while searching. + See #647' + - type: fix + text: Download status bar did not update properly when the downloader was idle + after a download. The bar will now be updated until either a new download is + started or the bar is properly filled, representing the downloader's idle state. + This should hopefully also fix the long-standing issue with the browser tab + freezing / crashing after a while. + - type: fix + text: Ensure that threads which send data to the frontend (like notifications + or downloader status) are only active when a UI session (=browser tab) is open. + Also only send downloader data if it's actually new (instead of e.g. repeatedly + sending information that the downloader is idle. + - type: fix + text: Remove code for nzbs.org :-( + final: true +- version: v3.11.4 + date: '2021-02-08' + changes: + - type: fix + text: Fix warning "Destroy method on bean..." when shutting down NZBHydra. + - type: fix + text: Fix automatic configuration of Sonarr and Radarr v3. + final: true +- version: v3.11.3 + date: '2021-01-31' + changes: + - type: fix + text: Fix an issue where the backup folder was not properly validated when saving + the config. Too bad I don't get paid by the update (=bug). + final: true +- version: v3.11.2 + date: '2021-01-31' + changes: + - type: fix + text: Fix a websocket issue when using a reverse proxy. Should've tested that + better... If you're running NZBHydra behind a reverse proxy please see https://github.com/theotherp/nzbhydra2/issues/683#issuecomment-770444576. + final: true +- version: v3.11.1 + date: '2021-01-31' + changes: + - type: fix + text: Introduced a stupid bug in v3.11.0 which prevented all but one particular + indexer from being selected. Sorry about that. + final: true +- version: v3.11.0 + date: '2021-01-31' + changes: + - type: feature + text: Implemented rate limiting for certain indexers which don't allow more than + x hits in x seconds. If you know of such an indexer please let me know as this + is hard coded and not configurable. + - type: feature + text: Added option to disable "What's new" button after an automatic update was + installed. + - type: fix + text: Validate backup folder when saving config. + - type: fix + text: Allow direct input for indexer color. + - type: fix + text: Fixed an issue where animetosho results would show up as a warning in the + log. The indexer contains NZB and/or torrent links combined in one feed. When + you made an NZB or torrent search and a result only contained a link for the + other type this would be shown as a warning. This message will now only be shown + on debug level. + - type: fix + text: Improve the connection check to sabNZBd so that false positives should be + reduced (.i.e in NZBHydra connecting successfully to a proxy or other server + is not interpreted as successful connection check). + final: true +- version: v3.10.1 + date: '2021-01-28' + changes: + - type: fix + text: Fix an issue with hydra using a base URL (e.g. /nzbhydra2). + final: true +- version: v3.10.0 + date: '2021-01-28' + changes: + - type: feature + text: Enabled compression for resources sent to the browser. This shouldn't matter + on local connections but save bandwidth should you want to use UI on a mobile + browser (horrible as it looks). + - type: feature + text: The GUI will now retrieve notifications, the downloader status and search + state via WebSocket. This means that the browser keeps a connection to the server + open and is only sent data when new data is available (e.g. when the downloader + status actually changed). This should result in considerably fewer requests + and (negligible) faster UI update times. + final: false +- version: v3.9.2 + date: '2021-01-16' + changes: + - type: feature + text: Added some code that allows me to post a link to adapt the logging config + and to download the debug infos. + - type: feature + text: Added config switch to add NZBs to downloader paused. + - type: fix + text: Added some more logging and handling of edge cases for API limits. + final: true +- version: v3.9.1 + date: '2021-01-16' + changes: + - type: fix + text: Fixed an issue with time zones related to indexer API limits. It may only + affect the log output but may also fix some problems with limit detection. + final: true +- version: v3.9.0 + date: '2020-12-28' + changes: + - type: feature + text: NZBHydra will now show the search results table even if all results were + rejected. This way you can see the reason for the rejections without having + to check the log. + - type: fix + text: Show advanced features in downloader config if selected. + final: true +- version: v3.8.1 + date: '2020-12-28' + changes: + - type: fix + text: Remove NZBGeek from list of domains for which do disable SNI. + - type: fix + text: Change text for toggle of advanced options in the config to "Advanced hidden" + and "Advanced shown". + final: true +- version: v3.8.0 + date: '2020-12-13' + changes: + - type: feature + text: Add 'Show advanced' switch to config. I'd already tried to get this working + twice - third time's the charm!. + - type: feature + text: Add button to clear color for an indexer. + - type: fix + text: Apply indexer colors to expanded results as well. To mark expanded results + they're shown in a darker shade so it's recommended to use indexer colors which + not only differ in lightness. + final: true +- version: v3.7.0 + date: '2020-12-13' + changes: + - type: feature + text: New display option to hide 'Results as ZIP' button. + - type: feature + text: New option to choose quickfilters that should be preselected. + - type: feature + text: New option to select the primary downloader for which the footer will show + the status. + - type: fix + text: Re-add 'No category' to category selection which got lost in 3.5.0. + - type: fix + text: The multiselect widgets in the config will now show the labels of the selected + values, not their internal ID. + - type: fix + text: "'Searching...' window was not closed when all found results were being + filtered." + - type: fix + text: Don't show the blue loading bar when checking for notifications. + - type: fix + text: Report API errors as JSON instead of XML when appropriate. + final: false +- version: v3.6.0 + date: '2020-12-05' + changes: + - type: feature + text: When aborting an indexer search because no ID conversion was possible Hydra + will now show a less... serious message. This is an expected problem, not an + error. + - type: fix + text: Remove ampersand (&) from titles when searching indexers as they're interpreted + specially. + final: true +- version: v3.5.1 + date: '2020-11-15' + changes: + - type: fix + text: Fix linux wrapper executable. + final: true +- version: v3.5.0 + date: '2020-11-15' + changes: + - type: feature + text: Use (bigger) buttons for downloader category selection. + - type: fix + text: Fix recognition of java version with recet OpenJDK update. + final: true +- version: v3.4.3 + date: '2020-10-31' + changes: + - type: fix + text: Fix error that ocurred when notifications without Apprise URLs were sent. + final: true +- version: v3.4.2 + date: '2020-10-29' + changes: + - type: fix + text: Fix error that ocurred when notifications without Apprise URLs were sent. + final: true +- version: v3.4.1 + date: '2020-10-25' + changes: + - type: fix + text: Fixed external configuration of Radarr and Sonarr V3 (wrt torrents). + final: true +- version: v3.4.0 + date: '2020-10-25' + changes: + - type: feature + text: Added age and source variables to download notification. + - type: feature + text: The previously added "Download" notification was only for when a result + was grabbed from Hydra. I've aded a notification for download completion, i.e. + when the download finishes the download. + - type: fix + text: Fixed external configuration of Radarr and Sonarr V3. + final: true +- version: v3.3.0 + date: '2020-10-22' + changes: + - type: feature + text: Added notifications for downloads. + - type: fix + text: The button to send results to the downloader was not displayed in some cases. + - type: fix + text: Apprise notifications sent via CLI containing quotation marks were truncated. + final: true +- version: v3.2.1 + date: '2020-10-21' + changes: + - type: fix + text: Prevent startup errors when migrating from certain older versions. + final: true +- version: v3.2.0 + date: '2020-10-20' + changes: + - type: feature + text: Allow using Apprise CLI to send notifications instead of Apprise API. + - type: fix + text: Don't require Apprise API URL to end with /notify (will still work if you've + already configured it that way). + - type: fix + text: Anonymize notification URLs when writing debug infos. + - type: fix + text: Prevent invalid expiry date setting and fix startup failing due to invalid + setting. + final: true +- version: v3.1.0 + date: '2020-10-18' + changes: + - type: feature + text: Add button to test notifications. + final: true +- version: v3.0.0 + date: '2020-10-17' + changes: + - type: feature + text: NZBHydra now allows to send and show notifications for certain events. You + can request events on the Github Issue. + - type: fix + text: Reduced the percentage of correct results an indexer must return for an + ID based search for that ID to be determined to be usable for searches. This + will hopefully make the caps check recognize more supported IDs without any + false positives. + - type: fix + text: Shorten torrent file names exceeding the maximum path length. + - type: fix + text: Query generation was not properly used for indexers which support a certain + search type but no IDs. + - type: fix + text: Show a warning when more than 3 logging markers are enabled. Please only + enabled them when requested by me. They reduce the performance and produce lots + of irritating log output which hurts more than it helps unless I actually need + it. + final: false +- version: v2.29.1 + date: '2020-09-12' + changes: + - type: fix + text: Use better name for indexer entries added to *arr. + - type: fix + text: Fix issue with indexer names containing special characters when configuring + *arr. + final: true +- version: v2.29.0 + date: '2020-09-08' + changes: + - type: feature + text: Added support for indexer priority when configuring Sonarr v3 and Radarr + v3. + - type: feature + text: Added support for automatic configuration of Readarr. + - type: fix + text: Anonymize API key and URLs when logging *arr requests and responses. + final: true +- version: v2.28.1 + date: '2020-09-08' + changes: + - type: feature + text: Added Lidarr and Readarr to list of known user agents. + - type: feature + text: The dialog for the configuration of external tools will now save the input + for each tool and restore it the next time you open the dialog for that tool + again. + - type: feature + text: Support for indexer VIP expiry date 'Lifetime'. No logic behind it, just + so you can enter and see that information. + - type: fix + text: Improve layout of quick filter buttons in search results. + final: true +- version: v2.28.0 + date: '2020-09-03' + changes: + - type: feature + text: When using "Add links" to add NZBs to your downloader the links are usually + calculated using the URL with which you accessed NZBHydra. This might be a URL + that's not accessible by the downloader (e.g. when it's inside a docker container). + You can now configure a URL in the downloading config that will be used for + these links instead. + - type: fix + text: Don't let the invisible update footer catch clicks meant for the elements + behind it. + final: true +- version: v2.27.2 + date: '2020-09-01' + changes: + - type: fix + text: Use proper version for Radarr v3 (I used a develop version which returns + 10.x). + - type: fix + text: Reduce log output of exceptions. + - type: fix + text: "(Hopefully) improve detection of local IP address when binding to 0.0.0.0." + final: true +- version: v2.27.1 + date: '2020-09-01' + changes: + - type: feature + text: Support for Radarr v3 (see v2.27.0). + final: true +- version: v2.27.0 + date: '2020-08-31' + changes: + - type: feature + text: You can now automatically add NZBHydra as an indexer to Sonarr / Radarr + / Lidarr. You can choose to add it as a single entry or one for every configured + indexer and if it should be added as newznab and/or torznab indexer. + - type: fix + text: Close search history dropdown in search dialog when it was dragged. + - type: fix + text: Make quick filters case insensitive. + final: false +- version: v2.26.0 + date: '2020-08-24' + changes: + - type: feature + text: Drag an entry from the search history to the search input to prefill it + with the history entry's values. Then you can adapt them and search. + - type: feature + text: Added quick filters for HEVC and x265. Added an option to always show quick + filter buttons (i.e. for any type of search). You can also define custom quick + filters. + - type: feature + text: Show number of filtered and number of duplicate results on search results + page. + - type: fix + text: Fix layout of search input for movies and shows. + - type: fix + text: Fix download of magnet links to black hole for some trackers. + final: true +- version: v2.25.0 + date: '2020-07-02' + changes: + - type: feature + text: When creating debug infos log all changes made to the config. + - type: feature + text: Show dl/ul ratio indicator for torznab results (if not 100%). E.g. when + '50%' is shown only half the download's size will be counted towards your ratio. + Freelech torrents will be shown as such. + final: true +- version: v2.24.1 + date: '2020-06-29' + changes: + - type: fix + text: Generate query for book searches if enabled and book search not supported + by indexer. + - type: fix + text: Autofocus search input field (pretty sure that worked at some point). + - type: fix + text: Catch illegal characters in hostname when configuring sabnzbd. + final: true +- version: v2.24.0 + date: '2020-06-22' + changes: + - type: note + text: I've upgraded some of the libraries I used. This should ideally not change + anything but to be sure I'll release this as prerelase first. + - type: fix + text: Remove API keys in URL encoded log entries. + final: false +- version: v2.23.0 + date: '2020-06-03' + changes: + - type: feature + text: Double-click system tray icon to open GUI in browser. + - type: feature + text: Click downloader image in footer to open it in a new tab. + - type: feature + text: Add toggle to display options for search result groups being expanded by + default. + - type: feature + text: Add toggle to display options for the indicator of already downloaded results. + - type: feature + text: Add toggle to display options to control display of already downloaded results. + It's basically a filter. + final: true +- version: v2.22.5 + date: '2020-05-27' + changes: + - type: fix + text: Indexers which report the API and download limits were not properly selected + when the hit limit was reached but the latest hit was more than 24 hours ago. + - type: fix + text: Error while searching Animetosho. + - type: fix + text: Properly recognize ID based searches returning too many results. + final: true +- version: v2.22.4 + date: '2020-05-24' + changes: + - type: fix + text: Fix problem with torznab introduced in last version (looking at me again, + in this case). + final: true +- version: v2.22.3 + date: '2020-05-24' + changes: + - type: fix + text: Fix problem with paging introduced in last version (looking at me, in this + case). + final: true +- version: v2.22.2 + date: '2020-05-20' + changes: + - type: feature + text: Add refresh buttons to search and download history. + - type: fix + text: Properly handle indexers which report more results in an API response than + they actually return (looking at you, wtfnzbs). + final: true +- version: v2.22.1 + date: '2020-05-13' + changes: + - type: fix + text: Last release was broken a bit... + final: true +- version: v2.22.0 + date: '2020-05-13' + changes: + - type: feature + text: 'Add filter for minimum # of seeders (in general and per tracker).' + - type: fix + text: It appears that the hashing algorithm used to check for the outdated wrapper + files behaves differently on some machines / OSes. I switched to SHA1 which + should reduce false positives. If you still get the wrapper warning and really + updated all files let me know. + - type: fix + text: Made sure that torznab results are never considered duplicates to anything. + It could be argued that in some cases two torrents from public trackers may + actually be the same but I consider that an edge case. + final: true +- version: v2.21.1 + date: '2020-05-09' + changes: + - type: feature + text: 'Make instructions what to update when your wrapper is outdated extra clear: + Any wrapper file found in the folder must be updated, not just the one you''re + using to run hydra. If the message says to extract the ZIP into your nzbhydra + folder I mean ALL THE FILES.' + - type: note + text: I moved my mail address from theotherp@gmx.de to theotherp@posteo.net. + final: true +- version: v2.21.0 + date: '2020-05-08' + changes: + - type: feature + text: NZBHydra will now try to fall back to similar results when an NZB download + fails. This is only possible if it proxies the results instead of redirecting + to the indexer so I've made that the default *for new installs*. It works by + looking for results with the same title from other indexers which were found + in the same search as the result of which the download failed. + - type: fix + text: Covers were not shown for search results. + - type: fix + text: Fix layout of tooltip icons ("?") in config in safari browser. + final: true +- version: v2.20.7 + date: '2020-05-07' + changes: + - type: fix + text: Execute check of outdated wrapper on startup to properly detect updated + wrapper. + - type: fix + text: Fix some more layout issues. + final: true +- version: v2.20.6 + date: '2020-05-07' + changes: + - type: fix + text: Revert tool to compile python wrapper to exe to older version as new exe + files were (falsely!) recognized as a virus by *some* tools. To be clear, the + files were never problematic. That means you'll have update the exe files or + python scripts again. + - type: fix + text: Fix decoding issue of settings file by python3 wrapper. + final: true +- version: v2.20.5 + date: '2020-05-06' + changes: + - type: feature + text: Mark results in GUI that already have been downloaded. + - type: fix + text: Fix issue with notification about outdated wrapper files not being shown. + NZBHydra will now nag you until you refresh the files. + - type: fix + text: Revert layout fixes made in v2.20.4 because fuck CSS. + final: true +- version: v2.20.4 + date: '2020-05-06' + changes: + - type: feature + text: GC logging (for debugging of memory issues) is now configurable and disabled + by default. This has required a change in the wrapper which means you'll have + to manually update them if you're running NZBHydra on windows or are using the + linux executable. + - type: feature + text: Show current version and (if applicable) docker container infos on about + page. + - type: fix + text: Fix parsing of API limits from indexers' API responses using different formats. + - type: fix + text: Don't show news for fresh installs. + - type: fix + text: Fix some (minor) layout issues on the search page. + final: true +- version: v2.20.3 + date: '2020-05-01' + changes: + - type: feature + text: NZBHydra will recognize renamed indexers when saving the config. Renaming + will no longer cause loss of stats and history for those indexers. You should + have two indexers configured with the same host, API key and search type as + this messes with the rename detection. + - type: feature + text: Click covers in search results to show them in a pop-up. + - type: fix + text: In some cases the download history could not be opened. + final: true +- version: v2.20.2 + date: '2020-04-29' + changes: + - type: feature + text: Added more substructures to the config GUI to make it a bit more clear. + - type: feature + text: Replaced the config help pop-up pages with contextual help. Click the question + marks neach to each field to get a bit more in-depth info. + - type: feature + text: Add button to clear search input. + - type: fix + text: Automatic update notification was also shown for manual updates. + final: true +- version: v2.20.1 + date: '2020-04-28' + changes: + - type: feature + text: Add button to debug infos tab to list all HTTP endpoints (useful for reverse + proxy config). + - type: fix + text: Search type SEARCH wasn't displayed in indexer config. + - type: fix + text: Improve matching of indexer configs when reading jackett config. + - type: fix + text: Restore display of button to send torrents to black hole. + - type: fix + text: Try to fix circular loading error when creating backup. + final: true +- version: v2.20.0 + date: '2020-04-26' + changes: + - type: feature + text: Option to filter out results by language. Very few indexers provide the + language in the results, though. + - type: feature + text: You can now add self-signed certificates for any hosts you want to connect + to. Just create a folder named 'certificates' inside the data folder, put your + .crt files there and reload NZBHydra. + - type: feature + text: Option to disable SSL verification for local hosts. (This was on by default + so far). + - type: feature + text: Support for saving NZBs to a black hole. + - type: fix + text: Fix sorting by age in download history. + - type: fix + text: NZBGet connection didn't honor SSL verification settings. + - type: fix + text: Properly display last errror on indexer statuses page. + - type: fix + text: Fix parsing of binsearch date on non-english locales. + - type: fix + text: Properly recognize duplicate NZBs not added to sabNZBd. + - type: fix + text: Improve matching of downloads to downloader entries where no external ID + exists, i.e. those downloads resulting from API accesses. + - type: fix + text: When using NZBGet the wrong NZB would be shown as downloading if the first + entry in the queue wasn't the one downloading. + - type: fix + text: Handle errors better while adding torrents to black hole or sending magnet + links. + - type: fix + text: Filter out quotation marks (") when searching NZBGeek. + - type: note + text: I've added two settings for the database. Just ignore them unless told otherwise + ;-) + final: true +- version: v2.19.6 + date: '2020-04-23' + changes: + - type: fix + text: With v2.15.0 I added the option to configure the backup folder and changed + the path from being relative to the data folder to being relative to the main + folder. That's not compatible with docker containers and broke the backup but + I always insisted it wasn't my fault - it was, sorry. + final: true +- version: v2.19.5 + date: '2020-04-20' + changes: + - type: fix + text: Indexer caps check was not executed when adding a new indexer. + - type: note + text: Happy 420. Stay inside. Stay healthy. Sorry for all the bugfix releases... + final: true +- version: v2.19.4 + date: '2020-04-20' + changes: + - type: fix + text: Fix error while reading API limits response from indexers which don't report + oldest access time. + final: true +- version: v2.19.3 + date: '2020-04-20' + changes: + - type: fix + text: Fix error when searching torznab. + final: true +- version: v2.19.2 + date: '2020-04-20' + changes: + - type: fix + text: Fix error related to fallback. + final: true +- version: v2.19.1 + date: '2020-04-20' + changes: + - type: feature + text: Add debug output for determination of API/download limits. + - type: fix + text: Corectly parse API/download limit information from NNTmux. + final: true +- version: v2.19.0 + date: '2020-04-19' + changes: + - type: feature + text: Add option to set VIP expiry date for an indexer. You will be warned when + the expiry date is near or has been reached. + final: false +- version: v2.18.0 + date: '2020-04-19' + changes: + - type: feature + text: Added option to define a color for an indexer. Results from that indexer + will be marked using that color. + - type: fix + text: Some indexers apparently return all results for ID based searches when actually + no results were found. In this case it will be handled as if no results were + found. + - type: fix + text: Adjust width of title box in search form when displaying results. + - type: fix + text: Fallback to query generation was often not executed when it should've. + final: false +- version: v2.17.6 + date: '2020-04-17' + changes: + - type: fix + text: Fix passworded releases not being included for a certain indexer. + - type: note + text: Added logging to debug query generation fallback. + - type: note + text: Added thank-you to newsgroup.ninja for sponsoring me. + final: true +- version: v2.17.5 + date: '2020-04-05' + changes: + - type: fix + text: Fix copy & paste error introduced with last version. + final: true +- version: v2.17.4 + date: '2020-04-05' + changes: + - type: fix + text: Min and max size were not filled on page load with a custom default category + was configured. + final: true +- version: v2.17.3 + date: '2020-04-01' + changes: + - type: feature + text: Add option to always convert media IDs. This might make sense for indexers + that only sometimes support a certain ID, i.e. don't have all results tagged + with a certain ID but may have tagged them with others. + final: true +- version: v2.17.2 + date: '2020-04-01' + changes: + - type: fix + text: Fix database error when searching with disabled history. + - type: fix + text: Don't show option how long to keep stats with disabled history. + final: true +- version: v2.17.1 + date: '2020-03-31' + changes: + - type: fix + text: When reading the jackett configuration update existing tracker configs instead + of replacing them. + - type: note + text: Enable query generation for internal searches by default. + final: true +- version: v2.17.0 + date: '2020-03-30' + changes: + - type: feature + text: Add option to read Jackett config and automatically add all configured trackers. + Already configured trackers will be updated. Thanks for Davide Campagna for + the tip. + - type: feature + text: Add option to configure downloader to never send a category. + - type: fix + text: Some time ago I implemented a feature that recognized OutOfMemory errors + in the log which might've been not recognized by the user because the program + automatically restarts after such a crash. Ironically this check caused OutOfMemory + errors with huge log files... + final: true +- version: v2.16.5 + date: '2020-03-23' + changes: + - type: feature + text: Enable some more logging to debug slow server responses. + final: true +- version: v2.16.3 + date: '2020-03-23' + changes: + - type: feature + text: Fancy new graph for CPU usage per thread in the debug infos section. Enable + the logging marker 'Performance' for the graph to show data. In this case NZBHydra + will also log memory usage and any threads using more than 5% CPU. This might + have an overhead on some systems so I don't recommend running it by default. + - type: feature + text: Add option to create and log a thread dump directly from the GUI. You can + also create a heap dump for me to analyze but this will only work with non-J9 + JREs. + - type: feature + text: Log more data in the debug report. If you're curious you can visit http://127.0.0.1:5076/actuator + (or whatever your IP and port are) and take a look. There's a lot of data you + could display in a dashboard. + final: true +- version: v2.16.2 + date: '2020-03-22' + changes: + - type: feature + text: Extend performance logging. + final: true +- version: v2.16.1 + date: '2020-03-15' + changes: + - type: fix + text: Fix display of version history. + final: false +- version: v2.16.0 + date: '2020-03-15' + changes: + - type: feature + text: Add an extra option for how long stats data is kept. Set this to 4 weeks + or so. I've also updated the wiki's memory page to explain which settings impact + the database size which is usually the reason for high memory usage. + - type: note + text: I've reduced the default for how long search results are kept in the database + to mitigate some memory issues with large databases. Only very few users should + be affected by this negatively. + - type: note + text: I've moved the settings for the history to the main config section. + final: false +- version: v2.15.2 + date: '2020-03-15' + changes: + - type: feature + text: Increased the number of entries in the search history dropdown to 25. + - type: feature + text: Added option to configure the number of results shown per page. Default + is 100. Max is 500. + - type: fix + text: Properly display full width of selected titles from the search autocomplete. + - type: fix + text: Remove apostrophe (') from generated queries again. Seems like there is + no right way to do this as some trackers return more results with and some without; + but it also seems that more indexers prefer it to be removed. If you know a + tracker / indexer that works better with apostrophes in the query please tell + me and I will make an exclusion for them. + final: false +- version: v2.15.1 + date: '2020-03-14' + changes: + - type: fix + text: Contents of the generic storage were not properly migrated. + final: false +- version: v2.15.0 + date: '2020-03-14' + changes: + - type: feature + text: Option to disable history of searches and downloads. + - type: feature + text: Option to configure folder for backups. + - type: feature + text: Option to not send newznab categories for torznab indexers (trackers). See + #516. + - type: note + text: Previously a couple of settings (Time of last backup, time of first start, + latest news shown, etc.) were stored in the database. That meant they were lost + when starting with a new database. I've moved the settings to the config file + wherey they belong. + - type: feature + text: Show a notification footer when an automatic update was installed. + - type: fix + text: Don't crash py3 wrapper when trying to log unicode characters. + - type: fix + text: Keep result selection when changing pages. (To be precise, the result selection + was actually already kept but upon page change "selected" results' checkboxes + would not be checked.) + - type: fix + text: Fix download of results as ZIP (which apparently nobody uses as it seems + to have been broken forever...). + final: false +- version: v2.14.2 + date: '2020-03-09' + changes: + - type: fix + text: Ensure passworded results are included for certain indexers when configured + not to ignore them. + - type: fix + text: Fix link to downloading help. + final: true +- version: v2.14.1 + date: '2020-02-23' + changes: + - type: fix + text: Fix failing startup of fresh instance on Linux. Thanks, hotio. + final: true +- version: v2.14.0 + date: '2020-02-23' + changes: + - type: feature + text: Allow setting a default category to be preselected. + - type: feature + text: Add supported SSL ciphers to debug infos output. + - type: feature + text: Update indexer presets. + - type: fix + text: Fix errors in py3 wrapper. + final: true +- version: v2.13.14 + date: '2020-02-19' + changes: + - type: fix + text: Fix error in URL calculation, resulting in failing API downloads when other + programs access Hydra via a reverse proxy with SSL. + final: true +- version: v2.13.13 + date: '2020-02-17' + changes: + - type: fix + text: Redirect system.out to log file for SSL debug infos even on windows + final: true +- version: v2.13.12 + date: '2020-02-15' + changes: + - type: fix + text: Don't use value of X-Forwarded-For when recognizing secure IPs. + - type: fix + text: Don't use search IDs in fallback queries. The indexer already returned 0 + results for the last search and providing them may prevent the query from returning + results. + final: false +- version: v2.13.11 + date: '2020-02-15' + changes: + - type: fix + text: Fix invalid config created by v2.13.9. + final: false +- version: v2.13.10 + date: '2020-02-15' + changes: + - type: fix + text: Fix error while saving config. + final: false +- version: v2.13.9 + date: '2020-02-15' + changes: + - type: feature + text: Replace auth token implementation with auth header. You may define an header + that provides a username and a range of IP addresses from which this header + will be accepted. The user will automatically be logged in. + final: false +- version: v2.13.8 + date: '2020-02-15' + changes: + - type: feature + text: Allow authorization via predefined OAuth2 / (X-)Authorization header. The + token must be unique for each user in order to identify him. + - type: fix + text: Improve matching of downloader history entries. + - type: fix + text: Use dereferer for links in config help texts. + - type: fix + text: Improve readability of error messages in dark and grey themes. + final: false +- version: v2.13.7 + date: '2020-02-11' + changes: + - type: fix + text: Further extend logging if logging marker 'HTTP Server' is selected. Don't + hide local IP addresses in log. Replace other IP files with hashes to hide them + but make them comparable. + final: true +- version: v2.13.6 + date: '2020-02-09' + changes: + - type: fix + text: Improve matching of hidden download entries from NZBGet history. + final: true +- version: v2.13.5 + date: '2020-02-09' + changes: + - type: feature + text: Extend logging if logging marker 'HTTP Server' is selected. + final: true +- version: v2.13.4 + date: '2020-02-08' + changes: + - type: fix + text: Improve wording of config description on how to apply newznab categories + by pressing enter. + - type: fix + text: Include hidden results from NZBGet history when checking download status. + That way entries removed by *arr and other programs will be considered, too. + final: true +- version: v2.13.3 + date: '2020-02-06' + changes: + - type: fix + text: Fixed a very rare issue where a file stored in temp directory could not + be read or deleted which prevented successful database migration. + final: true +- version: v2.13.2 + date: '2020-02-02' + changes: + - type: fix + text: Uniqueness score was not saved for torrent downloads. + - type: fix + text: Apostrophes were removed from generated queries resulting in less results. + final: true +- version: v2.13.1 + date: '2020-02-01' + changes: + - type: fix + text: Don't verify hostnames for hosts for which to ignore SSL certificate checks. + final: true +- version: v2.13.0 + date: '2020-02-01' + changes: + - type: note + text: I've removed the option to use the packaged cacerts file which will now + always be loaded by default. A future version will allow to add custom certificates + to the chain. Most likely this will not affect many users anyway. + - type: fix + text: Fixed issue where SSL verification was not properly disabled for some hosts. + Certificate checks are now also automatically disabled for local hosts. + final: false +- version: v2.12.8 + date: '2020-01-29' + changes: + - type: fix + text: Same shit, different release. Thanks to reloxx13 for helping me reproduce + this. + final: false +- version: v2.12.7 + date: '2020-01-29' + changes: + - type: fix + text: Fix another issue with database migration. I'll switch to a different approach + soon, this is too fragile. + final: false +- version: v2.12.6 + date: '2020-01-29' + changes: + - type: fix + text: Fix issue with new installations not starting due to updated database library. + So much for more stability... + final: false +- version: v2.12.5 + date: '2020-01-28' + changes: + - type: fix + text: Update database library. Should have no effect on you except hopefully more + stability. + final: false +- version: v2.12.4 + date: '2020-01-27' + changes: + - type: fix + text: Fix exception that occurs when an indexer's API limit was reached as reported + by the indexer but Hydra can't find any of those hits in its database. + final: true +- version: v2.12.3 + date: '2020-01-26' + changes: + - type: fix + text: Updated included SSL certificates. + - type: fix + text: Include beta releases in changelog when they have been released between + the currently installed final version and the newest final version. + final: true +- version: v2.12.2 + date: '2020-01-20' + changes: + - type: fix + text: Made some improvements in the way certificates are loaded. This should hopefully + improve connectivity with NZBGeek on systems where it previously failed (due + to incorrect JDK installations). + final: true +- version: v2.12.1 + date: '2020-01-19' + changes: + - type: fix + text: Fix error in indexer selection when indexers report their API hits but don't + report the expiry timestamp. + - type: fix + text: Caching results for external API didn't work with some combination of parameters. + final: false +- version: v2.12.0 + date: '2020-01-18' + changes: + - type: feature + text: Show API and download limit related values in the indexer statuses page. + - type: feature + text: If an indexer reports API and download limits and current hits in the response + (as far as I know only nntmux does this) this will be stored and used to determine + if the indexer's limits are reached. This will allow more precise results when + any other programs (or you) happen to make API calls or downloads that Hydra + is not aware of. As a fallback the logged API hits and downloads from the database + are used (as before). + final: false +- version: v2.11.2 + date: '2020-01-15' + changes: + - type: fix + text: Fix SSL logging introduced in v2.11.1 when running in linux. + final: true +- version: v2.11.1 + date: '2020-01-14' + changes: + - type: feature + text: Added code to help debug SSL / certificate issues when connecting to indexers. + - type: fix + text: Fix all versions in version history being displayed as beta. + - type: fix + text: Handle unexpected response when checking caps better. + final: true +- version: v2.11.0 + date: '2020-01-12' + changes: + - type: feature + text: Restored old "Load all results" behavior. Now when enabled Hydra will display + all already retrieved results from the cache. You still need to click "Load + all" on the search results page to load all results available from indexers, + resulting in more API hits. I've renamed the setting in the config to "Display + all cached results". + final: true +- version: v2.10.13 + date: '2020-01-11' + changes: + - type: feature + text: Provide wrapper file for Python 3. Would be nice if you could test it and + let me know if it works. + final: false +- version: v2.10.12 + date: '2020-01-11' + changes: + - type: feature + text: Add validation to category config that warns users when a category contains + a newznab main category (like 2000) and a subcategory already covered by that + (like 2020). + - type: feature + text: Provide wrapper file for Python 3. + - type: fix + text: Recognize error thrown when search IDs not supported by Animetosho. + final: false +- version: v2.10.11 + date: '2020-01-07' + changes: + - type: fix + text: Allow users with access to stats to see the downloader bar. Prevent error + message for the others. + final: false +- version: v2.10.10 + date: '2020-01-07' + changes: + - type: fix + text: Add missing UI config entry for backup added in last update. + final: false +- version: v2.10.9 + date: '2020-01-06' + changes: + - type: feature + text: Switched option for backup to an interval of days which allows you to have + a finer control over when backups are created (e.g. more often if your system + tends to crash...). + final: false +- version: v2.10.8 + date: '2020-01-05' + changes: + - type: feature + text: Added option to update to prereleases. If you enable this you will get 'beta' + releases which I consider kinda stable but which contain bigger changes which + might still break stuff. If you want to help with development please enable + this and report any problems you encounter. Please note that any older instance + older than v2.10.8 (this one) will always update to prereleases because they + don't know the difference. Docker containers by popular maintainers will soon + support prerelease tags (or already do so by now). + final: true +- version: v2.10.7 + date: '2020-01-05' + changes: + - type: note + text: Happy new year! + - type: fix + text: Fix memory leak when using a proxy. + final: true +- version: v2.10.6 + date: '2019-12-29' + changes: + - type: fix + text: The last changes made a pretty big change in the searching behavior with + the option 'Load all' enabled. I'm completely rolling that back until I have + an idea how to get what I want without causing excessive search behavior. + final: true +- version: v2.10.5 + date: '2019-12-29' + changes: + - type: fix + text: Last udate (which reverted 2.10.3) was incomplete. Sorry, still drowsy from + too much christmas food ;-) + final: true +- version: v2.10.4 + date: '2019-12-28' + changes: + - type: fix + text: Revert 'load all' change made with last version as it causes some search + loops. Will need to take a closer look. + final: true +- version: v2.10.3 + date: '2019-12-28' + changes: + - type: fix + text: With the option to load all results enabled now all available results will + actually be loaded. + - type: note + text: I've removed the feature to migrate from v1 (to reduce the install size + and memory usage a bit). It's still possible to migrate in older versions and + then update to a current version. + final: true +- version: v2.10.2 + date: '2019-12-04' + changes: + - type: feature + text: Add option to log HTTP server requests and their response times. The log + messages will be written to a file nzbhydra.serv.log and not contained in the + debug infos. They might help debug some performance related problems. + final: true +- version: v2.10.1 + date: '2019-12-01' + changes: + - type: fix + text: Fix db error when trying to save downloaded NZB. + final: true +- version: v2.10.0 + date: '2019-11-30' + changes: + - type: fix + text: I've changed (fixed) the way indexers are queried when searches are being + made. In essence this will fix paging, allowing Radarr/Sonarr to properly read + NZBHydra's results over multiple pages. What does that mean for you? *arr will + probably find more results when doing backlog searches and NZBHydra will do + more indexer searches, resulting in increased API hits (but not more than if + you had configured them directly in *arr). For more (technical) details see + this GitHub issue. + - type: fix + text: Recognize indexers reporting -1 for api or download limit for "unlimited". + - type: fix + text: Fixed a minor layout issue in the config. + - type: fix + text: Make sure "Keep history for ... weeks" is either empty or set to a positive + value. + - type: fix + text: The download history didn't load properly when the option to delete old + searches from the history was set. + final: true +- version: v2.9.5 + date: '2019-11-23' + changes: + - type: feature + text: 'I realised the indexer score is too complex to show in a chart and replaced + it with a table, that shows more information. It will now contain the average + uniqueness score, the number of unique downloads and the number of searches + which resulted in a download and where an indexer was involved. ' + final: true +- version: v2.9.4 + date: '2019-11-21' + changes: + - type: fix + text: Further improvements regarding uniqueness score. + final: true +- version: v2.9.3 + date: '2019-11-20' + changes: + - type: feature + text: Some indexers report their API and download limits in their XMLs. NZBHydra + will detect that when the indexer's caps are checked and will automatically + fill out the config accordingly (keeping already set values as they are). + final: true +- version: v2.9.2 + date: '2019-11-17' + changes: + - type: fix + text: Actually show version dates in the updates page... + final: true +- version: v2.9.1 + date: '2019-11-17' + changes: + - type: fix + text: Further adjustment to uniqueness score. + final: true +- version: v2.9.0 + date: '2019-11-17' + changes: + - type: feature + text: I've updated the indexer uniqueness score calculation so that indexers which + are often involved in searches get a higher score than those rarely involved. + I also found a bug in the way the data was stored to the database so the old + values will be removed. + - type: feature + text: The changelog now contains release dates along with the version. + - type: feature + text: Added option to disable update banner when running docker. + final: true +- version: v2.8.4 + date: '2019-11-14' + changes: + - type: fix + text: Cached torznab results were returned as wrong XML. Also cached torznab and + newznab queries could conflict. + final: true +- version: v2.8.3 + date: '2019-11-14' + changes: + - type: fix + text: Improve logging and handling of torznab/newznab XML transformation. + final: true +- version: v2.8.2 + date: '2019-11-13' + changes: + - type: feature + text: Support for pourcesoir.in, a french indexer. Their dev approached me with + an idea on how to work around laws forbidding hosting NZB files. The indexer + provides a certain text for each result using which you will find the result + using a raw search engine like binsearch. NZBHydra will display a search icon + in the search results GUI via which you can search for the result on Binsearch. + I'll be honest, I'm not sure how viable that approach is, but I'm open to new + ideas. + - type: fix + text: Fix pagination display error with. + final: true +- version: v2.8.1 + date: '2019-11-11' + changes: + - type: feature + text: Allow to define sorting for the search results via URL parameters. Use &sortby= + and, optionally, &sortdirection=asc or &sortdirection=desc. This will take preference + to the sorting settings saved in a cookie but not overwrite them. + final: true +- version: v2.8.0 + date: '2019-11-08' + changes: + - type: fix + text: Hydra will use proper HTTP status codes when NZB download fails to signal + that an indexer's API limit is reached. This will be recognized by *arr, which + will skip the release and try another one. This will also prevent *arr from + disabling NZBHydra in such cases. + - type: fix + text: Fix minor issue in indexer uniqueness score calculation. + final: true +- version: v2.7.7 + date: '2019-11-04' + changes: + - type: fix + text: Indexer uniqueness score had wrong axis labels in the stats page. + final: true +- version: v2.7.6 + date: '2019-11-04' + changes: + - type: feature + text: Restore the indexer uniqueness score introduced with 2.7.0 and then rolled + back due to database migration problems. The database is now restored on startup + which should prevent any migration errors. The startup will take a while for + this update. + final: true +- version: v2.7.5 + date: '2019-10-30' + changes: + - type: fix + text: Fix 'You're not allowed...' error caused by the fix in 2.7.4... :-/ + final: true +- version: v2.7.4 + date: '2019-10-29' + changes: + - type: fix + text: Fix 'You're not allowed...' error related to security cookie. Thanks to + /u/routhinator for the hint. + final: true +- version: v2.7.3 + date: '2019-10-27' + changes: + - type: feature + text: Added option to configure cover width in search results. + - type: fix + text: TMDB capabilities were not correctly checked. + - type: note + text: I've added instructions to send one-time donations via PayPal and recurring + donations via Github Sponsors. + final: true +- version: v2.7.2 + date: '2019-10-06' + changes: + - type: fix + text: I had to revert the changes from 2.7.0 because for some reason some databases + could not be migrated. I'll need to take a closer look first, sorry. + final: true +- version: v2.7.1 + date: '2019-10-06' + changes: + - type: fix + text: Hopefully fix a problem which might prevent a successful database migration + for some instances. + final: true +- version: v2.7.0 + date: '2019-10-06' + changes: + - type: feature + text: 'I''ve added a new statistics value called "Indexer result uniqueness score" + (which is is a mouthful, if you have a better name please let me know). This + score attempts to answer the question: Which indexer should I keep and which + can I let go? See the + wiki for more information. The score will only work for new downloads.' + final: true +- version: v2.6.18 + date: '2019-09-29' + changes: + - type: feature + text: Add global cache time config parameter + final: true +- version: v2.6.17 + date: '2019-09-07' + changes: + - type: feature + text: Allow indexers to be used only for API update queries + - type: feature + text: Allow regular expressions to be used in the search results title filter + final: true +- version: v2.6.16 + date: '2019-09-05' + changes: + - type: fix + text: Fix problems with special characters when using autocomplete + final: true +- version: v2.6.15 + date: '2019-09-04' + changes: + - type: fix + text: Fix form auth and remember-me cookies + final: true +- version: v2.6.14 + date: '2019-08-12' + changes: + - type: fix + text: Minor changes + final: true +- version: v2.6.13 + date: '2019-08-12' + changes: + - type: fix + text: Hopefully fix error with CookieTheftException introduced with v2.6.12 + final: true +- version: v2.6.12 + date: '2019-08-04' + changes: + - type: features + text: Allow limiting the indexers to be used via API. Use "&indexers=,". + - type: fix + text: Reduce how long sessions are kept open, possible reducing memory usage in + some cases + final: true +- version: v2.6.11 + date: '2019-06-09' + changes: + - type: fix + text: Improve handling and performance of wildcards for removal of trailing words + - type: fix + text: Added option to define how long Hydra will try to compress the database + file when shutting down. With big databases shutting down may take up to 15 + seconds by default. I'm still working on analyzing why some databases grow very + large. Until I've found a way to prevent the root cause this option may help + a bit but it will still require Hydra to shut down (or restart) + final: true +- version: v2.6.10 + date: '2019-05-21' + changes: + - type: fix + text: Changing result selection using "Invert selection" and "Select/deselect + all" wasn't properly registered, making mass download buttons unusable + - type: fix + text: Error on startup on headless windows server + final: true +- version: v2.6.9 + date: '2019-05-17' + changes: + - type: fix + text: Remove trailing didn't work with words containing "s"... How do you explain + stuff to that to non-programmers... + - type: fix + text: USe localhost:8080 as preset sabNZBd URL + final: true +- version: v2.6.8 + date: + changes: + - type: fix + text: IMDB link in search history was invalid + final: true +- version: v2.6.7 + date: '2019-05-15' + changes: + - type: fix + text: Hopefully fix corruption of nzbhydra.yml when machine crashes (or user switches + off power deliberately -.-) + final: true +- version: v2.6.6 + date: '2019-05-14' + changes: + - type: fix + text: Fix NoClassDefFoundError (only occurred with HTTP logging marker enabled) + final: true +- version: v2.6.5 + date: '2019-05-14' + changes: + - type: fix + text: Fix shift-click for selecting multiple results. + final: true +- version: v2.6.4 + date: '2019-05-14' + changes: + - type: fix + text: Searches will always use the IDs provided in API calls and not replace them + by different IDs provided by IMDB or TVMaze. In very few instances TVMaze had + wrong IDs mapped which resulted in wrong searches. + - type: fix + text: Entering domains to bypass when using proxy didn't work. + - type: fix + text: Selecting multiple results in the same title group was not accepted. + final: true +- version: v2.6.3 + date: '2019-05-13' + changes: + - type: feature + text: Extended logging for download status updates. + - type: feature + text: Allow wildcards in "Remove trailing..." + - type: fix + text: Disable HSTS security header + final: true +- version: v2.6.2 + date: '2019-05-05' + changes: + - type: fix + text: The warning that the wrapper is outdated will be also displayed in the updates + section. You can also choose to be reminded again. + - type: fix + text: Update link to Font Awesome in downloader config to the version actually + supported. + final: true +- version: v2.6.1 + date: '2019-05-02' + changes: + - type: feature + text: Allow indexers to be enabled for all searches but API update searches, i.e. + those periodically done by Sonarr and others to get the latest releases. + - type: fix + text: Correctly report torznab caps (taking into regard only torznab indexers). + Also disregard any disabled indexers or those not enabled for API searches and + include IDs convertible to any of the supported IDs. + final: true +- version: v2.6.0 + date: '2019-05-01' + changes: + - type: feature + text: Warn when using config that violates indexer rules and that will result + in your API account being disabled. + - type: feature + text: Support IMDB IDs for TV search. This seems to be supported by few indexers + but by many trackers. + - type: feature + text: Add option to ignore load limiting for internal searches. + - type: feature + text: Sort indexers in config by state first, then score, then name. + - type: fix + text: NZBHydra used to always report all ID types (e.g. IMDB IDs) in the caps + to be supported. Now IDs will only be reported as supported if either at least + one configured indexer supports it or query generation is enabled. + - type: fix + text: Prevent log file download from accessing files outside data folder. + - type: fix + text: Parse indexer results with provided passwords correctly (although they don't + follow the spec...). + final: true +- version: v2.5.9 + date: '2019-04-27' + changes: + - type: fix + text: 'I used a discord invitation link that expires after one day. Use this one: + [https://discord.gg/uh9W3rd](https://discord.gg/uh9W3rd).' + final: true +- version: v2.5.8 + date: '2019-04-27' + changes: + - type: feature + text: Recognize when an outdated wrapper is being used and ask the user to update + it manually. + - type: fix + text: Don't complain about mixed newznab and torznab results when adding Anime + Tosho. + - type: fix + text: Removed nzbs.org from the presets :-( RIP + final: true +- version: v2.5.7 + date: '2019-04-27' + changes: + - type: feature + text: Attempt to automatically detect certain problems and inform the user (admin) + about it. For now this will only detect OutOfMemory errors which cannot be properly + handled when they occur. + - type: fix + text: Disable the grouping of TV results by episode when searching for a specific + episode. Also show information about the grouping the first time it is used. + - type: note + text: The python wrapper nzbhydra2wrapper.py which is the main entry point for + the program is now included in the linux release. If you start Hydra using that + python file it will be updated automatically although changes will only take + effect after the next restart of the main process. + - type: note + text: 'I was asked for a discord channel. This is it: [https://discord.gg/uh9W3rd](https://discord.gg/uh9W3rd). + I can''t promise I''ll be the regularly but feel free to join. Some users there + and on reddit are always willing to help (thanks, guys!).' + final: true +- version: v2.5.6 + date: + changes: + - type: fix + text: Provide a (better) error message when clicking the infos for a show with + TVRage ID for which no infos could be found. + final: true +- version: v2.5.5 + date: '2019-04-17' + changes: + - type: feature + text: Option to log/display hosts instead of IP addresses. I haven't found a proper + way of testing this so let me know if it works ;-) + final: true +- version: v2.5.4 + date: '2019-04-16' + changes: + - type: fix + text: Allow empty movie searches for NZBPlanet which should result in covers being + shown. + final: true +- version: v2.5.3 + date: '2019-04-16' + changes: + - type: fix + text: Update of downloader status failed with newsbin (which claims to be compatible + with the sabnzbd API). + final: true +- version: v2.5.2 + date: '2019-04-15' + changes: + - type: fix + text: Minor downloader status bar related fixes. + final: true +- version: v2.5.1 + date: '2019-04-14' + changes: + - type: feature + text: Display status of configured downloader on the bottom of the page. This + can be disabled in the downloading config. If multiple downloaders are configured + the first one is used. + - type: fix + text: Toggling the grouping of TV episodes or the display of TV/movie covers will + take effect without having to reload the search. + final: true +- version: v2.4.4 + date: '2019-04-12' + changes: + - type: feature + text: Reduced font size across the board to fit more results / buttons / whatever + on the page. Let me know if it's too tiny :-) + - type: fix + text: Add 6box and NZBPlanet to list of indexers which do not support TV or movie + searches without identifiers. + final: true +- version: v2.4.3 + date: '2019-04-10' + changes: + - type: fix + text: Make sure that 100 rows are shown when grouping results (either by season/episode + or by title). + - type: fix + text: Passwords for users were not properly migrated from v1. + final: true +- version: v2.4.2 + date: '2019-04-10' + changes: + - type: fix + text: As is tradition every feature release (2.4.0) is followed by a couple of + bug fix releases... The tv episode sorting should not throw any errors now and + actually work properly :-) + final: true +- version: v2.4.1 + date: '2019-04-10' + changes: + - type: fix + text: Daily episodes (like 04/08) were not parsed correctly, resulting in an error + (see 2.4.0 feature). + final: true +- version: v2.4.0 + date: '2019-04-09' + changes: + - type: feature + text: When searching in the TV categories in the GUI by default the results will + be grouped by season & episode instead of by title. This should make it easier + to select one result for every episode which is usually what you want. This + behavior can be switched off in the display options (do a new search after the + switch). + - type: fix + text: Minor improvements to colors in bright theme. + final: true +- version: v2.3.22 + date: '2019-04-04' + changes: + - type: feature + text: Logging marker to log HTTPS related stuff on debug level. + - type: fix + text: Removed an SSL related parameter from the wrapper. I already did this months + ago but forgot to update the binary for linux. So if you have problems with + SSL and are running Hydra on linux (not in docker) you might want to update + the binary. This needs to be done manually. + final: true +- version: v2.3.21 + date: '2019-03-31' + changes: + - type: feature + text: Option to send the mapped category name to downloaders. + - type: fix + text: "/api/stats/indexers endpoint was accessible without authorization." + - type: fix + text: Show unit for average response times in stats (ms). + final: true +- version: v2.3.20 + date: + changes: + - type: fix + text: Revert revert because, as it turns out, it wasn't the libary at fault but + the new version just failed to read a file already corrupted. + final: true +- version: v2.3.19 + date: '2019-03-20' + changes: + - type: fix + text: Revert update of database library as it caused errors on startup in some + issues. + final: true +- version: v2.3.18 + date: '2019-03-18' + changes: + - type: fix + text: Not all API keys were anonymized when creating the debug infos. + final: true +- version: v2.3.17 + date: '2019-03-17' + changes: + - type: feature + text: Binsearch is knowing for returning a 503 error every now and then. In that + case Hydra will retry the search up to two times. + - type: fix + text: An indexer not selected due to load limiting was displayed as being disabled + in the GUI. + - type: fix + text: Reduce frequency of config file being written. + final: true +- version: v2.3.16 + date: '2019-03-14' + changes: + - type: fix + text: Add database index to improve loading of search history on initial page + load. + - type: fix + text: Try to prevent config file from being corrupted. + final: true +- version: v2.3.15 + date: '2019-02-16' + changes: + - type: note + text: 'I need to make something clear: If Hydra shows you 100 results on the GUI + and says that x results are not yet loaded then that means that some results + you''re looking for may be missing. You will always only get the newest 100 + results from any indexer at first. Even if you sort by name then other results + which should be somewhere in that list may be ''hidden'' because they were not + yet retrieved from the indexer.' + - type: + text: Delay writing of config file so that not too many concurrent writes occur. + This should hopefully reduce the risk of file corruption. + final: true +- version: v2.3.14 + date: '2019-02-15' + changes: + - type: fix + text: Change how SNI verification is disabled so that nzbgeek.info should work + with Java 10+. + - type: fix + text: Fix NZBIndex parsing. Thanks to BenoitCharret. + final: true +- version: v2.3.13 + date: '2019-02-12' + changes: + - type: feature + text: Improve HTTP debug logging + - type: fix + text: Revert some more SSL related changes. If you still have problems connecting + to indexer please manually update the binaries. Unfortunately the update process + can't do that. + final: true +- version: v2.3.12 + date: '2019-02-10' + changes: + - type: fix + text: I don't know if I should laugh or cry, but the last version actually made + matters worse as 2.3.11 is unable to connect to GitHub (among others) which + disables the built in update function. So if you read this and don't run docker, + you'll have to update manually. + final: true +- version: v2.3.11 + date: '2019-02-10' + changes: + - type: fix + text: Cautiously optimistic that *some* SSL issues have been solved... ;-) + - type: fix + text: When implementing the display of covers I managed to mistakenly think that + posters and covers are the same. Actually the poster in this context is the + uploader but my code used the poster (username) as cover URL. If you've disabled + the display of 'posters' in the search results you'll have to disable it again. + final: true +- version: v2.3.10 + date: '2019-02-09' + changes: + - type: fix + text: Fix another issue with SSL. I should probably pause development until I'm + fit of mind enough to do this properly... + final: true +- version: v2.3.9 + date: '2019-02-09' + changes: + - type: fix + text: Revert SSL changes made in 2.3.7 as Hydra didn't start for some users. I + give up. + final: true +- version: v2.3.8 + date: '2019-02-09' + changes: + - type: fix + text: Updated executable to provide a java flag which should fix SSL related problems + introduced with 2.3.7. If you're not running Hydra inside a container you may + need to manually update the binary (nzbhydra*.exe or just nzbhydra on linux) + final: true +- version: v2.3.7 + date: '2019-02-09' + changes: + - type: fix + text: Changed the way SSL certificates are checked. Connection to indexers like + NZBGeek or althub should now work as expected. Removed the option 'Disable SNI'. + - type: fix + text: Count API hits used for connection and caps checks when calculating hit + limits. + - type: fix + text: When results are sorted by title the title groups are now sorted by indexer + score instead of age, meaning results from the indexer with the highest score + are shown when the title group is collapsed. + final: true +- version: v2.3.6 + date: '2019-02-06' + changes: + - type: fix + text: The audio category was preconfigured to require both mp3 and flac in the + results which doesn't make any sense. You might want to remove them in your + category config. + - type: fix + text: Old downloads were not removed from history even if the option to only keep + them for a certain time was set. + - type: fix + text: Check cover/poster URLs provided by indexers to catch some invalid URLs. + final: true +- version: v2.3.5 + date: '2019-02-05' + changes: + - type: feature + text: Show posters for movie results. Can be toggled in the display options. + final: true +- version: v2.3.4 + date: '2019-01-31' + changes: + - type: fix + text: Move cancel button in dialog shown while searching because you're all too + slow to click it. + - type: fix + text: Prevent database trace file becoming too large + - type: fix + text: Keep less gclog files in the log folder + final: true +- version: v2.3.3 + date: '2019-01-28' + changes: + - type: fix + text: Connection to hosts like 'sabnzd' would fail + final: true +- version: v2.3.2 + date: '2019-01-27' + changes: + - type: fix + text: 2.3.1 didn't start for users updating from 2.2.5 to 2.3.1. Fuck this shit + final: true +- version: v2.3.1 + date: '2019-01-27' + changes: + - type: fix + text: 2.2.5 unfortunately may have caused database corruption in some cases. Hopefully + no more... The fix may need some time the first time this new version is started. + final: true +- version: v2.3.0 + date: '2019-01-27' + changes: + - type: feature + text: Java 11 is now supported. This required an update of the internal framework + which might have some unforseen side effects (bugs), especially regarding authentification + and handling of reverse proxies. Let me know if something doesn't work as expected. + - type: feature + text: Rename searching option 'Ignore temporarily disabled' to 'Ignore temporary + errors'. If enabled indexers will not be temporarily disabled at all if a recoverable + error occurs. + - type: fix + text: Opening magnet links under Windows 7 doesn't require administrator rights + anymore. + final: true +- version: v2.2.5 + date: '2019-01-26' + changes: + - type: fix + text: In some cases (with really big databases) the check of the API hit limit + could take very long. This was hopefully improved. Migration to this version + might take a bit for such instances. + final: true +- version: v2.2.4 + date: '2019-01-26' + changes: + - type: feature + text: 'Add indexer specific limit to caps check. Background: RARBG only allows + one request every two seconds so the caps check, which until now used two concurrent + threads and a delay of 1 second, would result in errors. The limits are hard + coded. Hydra will not attempt to do any rate limiting for regular search requests.' + final: true +- version: v2.2.3 + date: '2019-01-26' + changes: + - type: feature + text: Include database metadata in debug infos + final: true +- version: v2.2.2 + date: '2019-01-26' + changes: + - type: feature + text: Minor improvements to performance logging + final: true +- version: v2.2.1 + date: '2019-01-21' + changes: + - type: feature + text: Improve logging of unparseable indexer responses + final: true +- version: v2.2.0 + date: '2019-01-02' + changes: + - type: note + text: This release brings some major changes regarding categories and the handling + of newznab categories. Please let me know if it breaks anything or has unexpected + side effects (or if you love what I've done ;-)) + - type: feature + text: Allow combinations of newznab categories which must all be found in a search + result for that category to be applied. For example 4090&11000 will only match + items with both 4090 and 11000. This should allow for even finer category tuning + with trackers accessed via Jackett. + - type: feature + text: Replace newznab categories incoming API searches with newznab categories + of mapped category. For example when you have 2040,2050 configured for Movies + HD and a search comes in using 2040 then indexers will be queried using 2040,2050. + Until now only the supplied category was used (2040 in the example). This should + result in more results to be found and so far I can't tell if it will return + just better results or more crap. You can disable this with the 'Transform newznab + categories' setting in the searching config. + - type: feature + text: 'Related to above: The categories on the caps page are created from the + configured categories. To keep this clean only one newznab category will be + used for every category (e.g. Movies HD using 2040,2050 will only be included + once with 2040 as ID.' + - type: fix + text: Use dereferer for NZB details site + final: true +- version: v2.1.7 + date: '2018-12-30' + changes: + - type: fix + text: Fix/improve category mapping introduced in 2.1.6. Use custom newznab categories + if none from the predefined range are provided. + final: true +- version: v2.1.6 + date: '2018-12-30' + changes: + - type: fix + text: When uploading a backup file the UI didn't update to inform the user about + the progress after the file was uploaded. + - type: fix + text: Improve category mapping for (torznab) indexers. Some use custom newznab + category numbers (>9999) which could not be properly mapped to preconfigured + categories. + final: true +- version: v2.1.5 + date: '2018-12-29' + changes: + - type: fix + text: Improve handling of movie and tv searches with some indexers (see v2.0.23). + I just wish all indexers could work the same... :-/ + - type: fix + text: Prevent indexers without caps from being caps checked (NZBIndex, Binsearch) + - type: fix + text: Improve wording indexer state when disabled by the system due to an error + from which it cannot recover automatically + final: true +- version: v2.1.4 + date: '2018-12-28' + changes: + - type: feature + text: Allow retrieval of history and stats via API. See https://github.com/theotherp/nzbhydra2/wiki/External-API,-RSS-and-cached-queries + - type: fix + text: Repeat of searches from history sometimes used wrong parameters + - type: fix + text: Added nzbs.org to list of indexers unable to process type searches without + IDs + final: true +- version: v2.1.3 + date: '2018-12-27' + changes: + - type: fix + text: Removed dead indexers from presets + - type: fix + text: Prevent exception related to duplicate TV infos in database + final: true +- version: v2.1.2 + date: '2018-12-18' + changes: + - type: fix + text: Indexer added as newznab indexer even when selected as torznab in the config + GUI + final: true +- version: v2.1.1 + date: '2018-12-18' + changes: + - type: fix + text: Validate config to prevent indexers with duplicate names + - type: fix + text: Validate config to prevent torznab indexers being added as newznab indexer + and vice versa + final: true +- version: v2.1.0 + date: '2018-12-15' + changes: + - type: fix + text: Search query was not built properly when conversion of search IDs did not + provide any IDs usable by an indexer + - type: feature + text: Support API caps in JSON + final: true +- version: v2.0.24 + date: '2018-12-14' + changes: + - type: note + text: Added NZBGeek to the list mentioned in v2.0.23. Thanks to the user letting + me know about it. + final: true +- version: v2.0.23 + date: '2018-12-11' + changes: + - type: note + text: Previously when an API call with search type 'movie' or 'tvsearch' was made + without any identifiers or category I would call indexers with search type 'search' + instead because some indexers don't like that. This causes some other problems + so I've reverted that behavior except for a certain list of indexers. I have + hardcoded list of indexers for which the switch will be done. I'm not sure which + indexers actually behave that way. So if you find an indexer where browsing + the movie or TV releases (e.g. using NZB360) will return a lot of crap please + let me know so I can add the indexer to the list. + - type: note + text: I've changed the java runtime that is used in the docker container maintained + by me (although I actually don't want to really support that...). In my tests + it nearly halved memory usage in some scenarios (199MB compared to 380MB). If + this proves to be stable I'll recommend the other maintainers to use this as + well. + final: true +- version: v2.0.22 + date: '2018-12-09' + changes: + - type: fix + text: Upload of large ZIP files for restoration was disabled + final: true +- version: v2.0.21 + date: '2018-12-09' + changes: + - type: feature + text: Some users have reported corrupted config files. I can't explain how that + could ever happen but I've added code that tries to recognize this on startuppu + and attempts to repair it automatically + - type: fix + text: New instances were not properly initialized, in some instances resulting + in a crash on startup. Sorry about that + final: true +- version: v2.0.20 + date: '2018-12-08' + changes: + - type: feature + text: Make sure existing configuration or database is not loaded by an older version + of a program than it was created with + final: true +- version: v2.0.19 + date: '2018-12-08' + changes: + - type: fix + text: Restoration from uploaded backup file wouldn't work + final: true +- version: v2.0.18 + date: '2018-12-05' + changes: + - type: fix + text: Details link was hidden even if not restricted by auth config + - type: fix + text: Redirects to torrent magnet links are now recognized and properly handled + - type: fix + text: Downloads of NZBs with spaces in the filename are now properly handled + - type: fix + text: Suffix NZBs sent to sabnzbd with .nzb to increase compatibility with newsbin + final: true +- version: v2.0.17 + date: '2018-11-22' + changes: + - type: feature + text: Automatic update. This feature has been requested for ages. Ironically, + now that I rarely release new versions I've finally implemented it. It's opt-in + for now even though the update process has been really stable for a while. Now + that the startup is faster Hydra shouldn't be unavailable during the update + process for more than 20 seconds or so. Any tools calling during that time should + recover fine. + - type: fix + text: Make sure to load resources from TVMaze using HTTPS + - type: fix + text: Handle (invalid) spaces in URLs + final: true +- version: v2.0.16 + date: '2018-11-21' + changes: + - type: fix + text: Size tag was not forwarded from torznab results + final: true +- version: v2.0.15 + date: '2018-11-02' + changes: + - type: fix + text: ID lookup for TV shows didn't always work + final: true +- version: v2.0.14 + date: '2018-11-02' + changes: + - type: note + text: The URL base has to start with a / from now on. Configs without URL base + will be migrated + - type: fix + text: ID based TV search from GUI would sometimes ignore ID + final: true +- version: v2.0.13 + date: '2018-10-26' + changes: + - type: feature + text: Warn when changing the host to an invalid IP + - type: fix + text: api.althub.co.za should hopefully actually work now + final: true +- version: v2.0.12 + date: '2018-10-24' + changes: + - type: fix + text: SSL error when accessing althub from docker. Should be fixed with the setting + to use the packaged cacerts file enabled + - type: fix + text: Detection and handling of required restart after changing config was broken + final: true +- version: v2.0.11 + date: '2018-10-23' + changes: + - type: feature + text: Allow to disable SSL verification only for certain hosts + - type: feature + text: Warn when host is changed from 0.0.0.0 and run in docker. This seems to + cause some problems + final: true +- version: v2.0.10 + date: '2018-10-20' + changes: + - type: fix + text: Sometimes search IDs would be used even if the indexer wasn't configured + to use them, resulting in failing searches + final: true +- version: v2.0.9 + date: '2018-10-06' + changes: + - type: fix + text: Caps check with Jackett indexers wouldn't complete properly due to a change + in their code + final: true +- version: v2.0.8 + date: '2018-10-01' + changes: + - type: fix + text: Adapt database to store long torrent magnet links + final: true +- version: v2.0.7 + date: + changes: + - type: fix + text: Sabnzbd API key was not migrated + final: true +- version: v2.0.6 + date: '2018-09-26' + changes: + - type: fix + text: Torznab queries were limited to 100 results. I've removed the limit altogether. + As torznab doesn't require or support paging there's no reason for a request + limit + final: true +- version: v2.0.5 + date: '2018-09-14' + changes: + - type: fix + text: Adding to downloader via result button would always show failed (introduced + with 2.0.3) + final: true +- version: v2.0.4 + date: '2018-09-13' + changes: + - type: fix + text: Improved feedback when adding NZBs to downloader failed + final: true +- version: v2.0.3 + date: '2018-09-05' + changes: + - type: fix + text: In some cases an incorrect NZB URL was used for downloads + - type: fix + text: Saving the config would sometimes show confusing or wrong warnings + - type: fix + text: Restoring from web UI had no effect + - type: fix + text: Category mapping would sometimes not work for incoming searches + final: true +- version: v2.0.2 + date: + changes: + - type: fix + text: Minor stability improvements + final: true +- version: v2.0.1 + date: '2018-08-19' + changes: + - type: fix + text: New installations would generate a faulty default configuration, resulting + in failed searches + final: true +- version: v2.0.0 + date: '2018-08-18' + changes: + - type: feature + text: NZBHydra 2 can now run with Java 8, 9 or 10. It shouldn't matter much which + version you use as long as it's up to date. If you want to use 9 or 10 you'll + need to manually update the wrapper (i.e. the executable(s) in the main folder) + - type: feature + text: Reduced startup time. My instance starts in 8 seconds instead of 22 but + YRMV + - type: feature + text: I updated the underlying libraries and main framework. This doesn't change + much for you except that NZBHydra 2 is a bit more future proof and may have + some new bugs :-) + - type: feature + text: Added an option to keep the history (searches, downloads, stats) only for + a certain time (see Searching options). This may reduce the database size and + stats calculation time and may improve performance a bit. + - type: fix + text: Hydra will correctly recognize if run in the windows program files folder + - type: fix + text: When shutting down or restarting Hydra will try to defrag the database file. + In some cases this should drastically reduce the database size. It may grow + again but for now I don't have a better fix than restarting the instance... + - type: fix + text: Remove multiple trailing words from titles if found + final: true +- version: v1.5.2 + date: '2018-07-31' + changes: + - type: fix + text: Adding new categories resulted in an exception + - type: note + text: Increased the default XMX value to 256 + final: true +- version: v1.5.1 + date: '2018-06-11' + changes: + - type: fix + text: Adding of downloaders to config was broken with last version + final: true +- version: v1.5.0 + date: '2018-06-10' + changes: + - type: feature + text: Redesigned the button to add new indexers. Inspired by Sonarr + - type: feature + text: When a torrent black hole is configured magnet links will be saved as files + there. Let me know if you need a switch to disable that. Thanks to wh0cares + - type: fix + text: Config validation was not executed properly, sometimes allowing invalid + values or even preventing the config from being changed + - type: note + text: Added a small note to the readme that "linux" releases mean any platform + but windows. Renaming the releases would break updates for running instances + final: true +- version: v1.4.18 + date: '2018-05-22' + changes: + - type: fix + text: Previous version was missing readme.md which resulted in broken updates + final: true +- version: v1.4.16 + date: '2018-05-22' + changes: + - type: fix + text: Small error in API? help from last version + final: true +- version: v1.4.15 + date: '2018-05-22' + changes: + - type: feature + text: Support animetosho (both newznab and torznab) + - type: feature + text: Add small 'API?' button in config to display newznab and torznab endpoints + and the api key + final: true +- version: v1.4.14 + date: '2018-05-21' + changes: + - type: fix + text: Error with TMDB IDs introduced with last version + - type: note + text: In some cases long running instances of Hydra use a lot of CPU when they + should be idle. I've made some changes which should reduce the problem to a + degree. Please let me know at https://github.com/theotherp/nzbhydra2/issues/96 + if you have similar problems or, even better, if they've gone away with this + version + final: true +- version: v1.4.13 + date: '2018-05-12' + changes: + - type: fix + text: Conversion of IMDB to TMDB ID failed with missing tt prefix + final: true +- version: v1.4.12 + date: '2018-05-05' + changes: + - type: fix + text: Prevent database error when ignoring too many updates... + final: true +- version: v1.4.11 + date: '2018-05-05' + changes: + - type: fix + text: Prevent rare database error when converting between movie IDs + - type: fix + text: Prevent API keys from leaking in debug infos ZIP when included in last error + property + final: true +- version: v1.4.10 + date: '2018-05-05' + changes: + - type: feature + text: Option to disable download status updates. *Might* help in some rare cases + where CPU usage is high when NZBHydra2 is supposed to idle + final: true +- version: v1.4.9 + date: + changes: + - type: fix + text: Log levels for console and file were not honored properly. + final: true +- version: v1.4.8 + date: '2018-03-17' + changes: + - type: note + text: Updated the wrapper to create a memory dump file if the main process crashes + when it's out of memory. As before you need to update the wrapper manually (except + when you use docker and don't use the internal update mechanism). This is not + strictly necessary but will improve chances of me debugging memory problems. + final: true +- version: v1.4.7 + date: '2018-03-14' + changes: + - type: fix + text: Bug in internal logic would throw exception and cause indexers to be disabled + for no reason + - type: fix + text: API hit limit reached on omg would disable indexer permanently + - type: fix + text: Indexer config state would change when switching config tabs + - type: fix + text: Indexer priority field was not displayed in config + final: true +- version: v1.4.6 + date: '2018-03-08' + changes: + - type: feature + text: Prepend words in the results filter box with ! to exclude them + - type: fix + text: Shift-click for selecting multiple results in a row didn't work on firefox + final: true +- version: v1.4.5 + date: '2018-03-05' + changes: + - type: fix + text: Improve caps check for some results using a TV show's initialism instead + of the full name in the title + final: true +- version: v1.4.4 + date: '2018-02-27' + changes: + - type: fix + text: Handle LL searches better that request a general category and a subcategory + (e.g. 7000,7020) + final: true +- version: v1.4.3 + date: '2018-02-23' + changes: + - type: fix + text: Migration failed because of missing datatabase table + final: true +- version: v1.4.2 + date: '2018-02-17' + changes: + - type: fix + text: Allow configuration of basic auth credentials for jackett + final: true +- version: v1.4.1 + date: '2018-02-13' + changes: + - type: fix + text: Indexers with incomplete config were shown in selection list but not actually + usable + - type: fix + text: Some issues with indexers not beeing reenabled and some confusing messages + being shown. The whole thing with indexers being disabled after errors is still + a bit wonky + - type: fix + text: Some potential memory leaks + final: true +- version: v1.4.0 + date: '2018-02-10' + changes: + - type: feature + text: Rewrote the display of indexer statuses. An indexer's status is now displayed + in the indexer config section (where you would probably expect it). The 'Enabled' + switch was extended and now will show one of the states 'Enabled', 'Temporarily + disabled', 'Permanently disabled' or 'User disabled' and an explanation. THe + Indexer statuses view does still show alle the indexers' statuses but is less + cluttered + - type: feature + text: Show search results filter box in table header because some users didn't + find the filter icons + - type: fix + text: Prevent weird 'Unexpected error in hydra code. Sorry...' + final: true +- version: v1.3.3 + date: '2018-02-08' + changes: + - type: feature + text: Improve conversion of newznab categories to internal categories + - type: fix + text: Exception in migration when providing no database file even when migration + of database was requested + - type: feature + text: Allow loading of UI files from local folder to allow proper development + of UI + final: true +- version: v1.3.2 + date: '2018-02-05' + changes: + - type: fix + text: Settings file was sometimes corrupted (wrong charset) and could not be loaded + anymore + - type: fix + text: Delete error column in indexer status page when indexer is reenabled + - type: fix + text: Button to browse file system for selecting torrent folder would fail on + some systems (e.g. docker) + final: true +- version: v1.3.1 + date: '2018-02-04' + changes: + - type: feature + text: Display serious errors on windows in message box + - type: fix + text: Hopefully reduced chance of empty config files being written + - type: fix + text: Handle duplicate results from indexers better (should rarely happen) + - type: note + text: NZBHydra will recognize if it's running on windows and in folder like c:\program + files or c:\program files (x86) and refuse to start. Those folders have special + read/write rights which might cause some problems. I recommend putting any programs + that are not installed by a setup in a "regular" folder + final: true +- version: v1.3.0 + date: '2018-02-03' + changes: + - type: feature + text: Experimental feature to use a packaged CA certs file. This probably doesn't + concern you but it may solve some SSL related issues with some newer or different + JREs + - type: fix + text: Sort indexer download shares by share + - type: fix + text: Made the migration process a tiny bit more robust wrt wrong input + - type: fix + text: Display caps check button for indexers without API key (e.g. spotweb instances). + Hide button and search type and ID fields for new indexer. The check is done + automatically + final: true +- version: v1.2.6 + date: '2018-02-02' + changes: + - type: fix + text: Sabnzbd history could not be properly parsed, preventing download status + updates + final: true +- version: v1.2.5 + date: '2018-01-31' + changes: + - type: fix + text: Completely fix spotweb support... + final: true +- version: v1.2.4 + date: '2018-01-31' + changes: + - type: fix + text: Help headphones parse Hydra's results + - type: fix + text: Indexer connection check used empty API key parameter, preventing check + to spotweb to work + final: true +- version: v1.2.3 + date: '2018-01-30' + changes: + - type: fix + text: Prevent session timeout + final: true +- version: v1.2.2 + date: '2018-01-29' + changes: + - type: note + text: I've added debug logging to the wrapper for better, well, debugging of problems + related to updating. To enable debug logging create a file DEBUG in the data + folder and restart the program. As before, any non-docker installations will + need to update the wrapper files manually. I'm working on a better solution. + - type: fix + text: Adding binsearch/NZBIndex/anizb would fail the connection check + - type: fix + text: Periodic check of downloader status was not executed as expected, resulting + in incomplete status NZB reports in the history + - type: fix + text: Logger sometimes swallowed information when anonymizing data + final: true +- version: v1.2.1 + date: '2018-01-27' + changes: + - type: note + text: I've changed how some data is kept in the database. Deleting an indexer + will remove it completely from the database, also deleting all related stats, + search results and downloads. This might take a while on the next startup or + whenever you delete an indexer with many related entries + - type: feature + text: Option to delete backups after x weeks. 4 is the default + - type: fix + text: Improve layout on mobile devices. Thanks nemchik + - type: fix + text: Updated the wrapper to delete older JAR files which previously caused some + trouble. Any existing installations will have to update this manually. Docker + containers must be updated. + final: true +- version: v1.2.0 + date: '2018-01-25' + changes: + - type: feature + text: Send torrent magnet links to associated program + - type: fix + text: Results without recognizable category were rejected + final: true +- version: v1.1.4 + date: '2018-01-22' + changes: + - type: fix + text: Hide torrent black hole buttons for magnet links + - type: fix + text: Torrents were sometimes not correctly downloaded and would have extension + .nzb + final: true +- version: v1.1.3 + date: '2018-01-21' + changes: + - type: fix + text: Fix NZB links not being constructed correctly. Sorry about that + final: true +- version: v1.1.2 + date: '2018-01-21' + changes: + - type: feature + text: Improved handling of XML generation for newznab/torznab API calls. Should + improve compatibility with calling tools + - type: feature + text: Hydra attempts to recognize if it's running inside docker. It will not allow + you call the internal update mechanism from the main page. You may still call + it from the Updates page but a warning will be shown. Let me know if this works + - type: fix + text: The URL code change introduced with 1.1.0 might've caused some problems + and should be fixed now + - type: fix + text: Sending NZBs from the download history to downloaders didn't work. You'll + have to manually choose a category because the original category isn't available + in the download history anymore + - type: fix + text: NZB filenames were not sanitized before being written to ZIP, resulting + in an error + - type: fix + text: Improved dialog during update installation (no more error messages when + everything is fine, hopefully) + - type: fix + text: Download history was not filterable by indexer + - type: fix + text: SickBeard/-rage/Medusa did not find all relevant categories. I've changed + the way Hydra reports itscategories to calling tools. It follows the predefined + categories of the newznab standard. + final: true +- version: v1.1.1 + date: '2018-01-17' + changes: + - type: fix + text: Fix results not being recognized by SickRage + - type: fix + text: The URL code change introduced with 1.1.0 might've caused some problems + and should be fixed now + final: true +- version: v1.1.0 + date: '2018-01-15' + changes: + - type: feature + text: Completely rewrote handling of scheme, port, host and context path. Should + solve some issues and prevent others from happening where reverse proxies are + involved. Also extended the Wiki. + There's no need to set an external URL anymore. Please report back if this causes + any issues + - type: note + text: I'll remove the option to send links to downloaders in one of the coming + versions. Only upload of NZBs to downloaders will be supported. v2 is capable + of handling it without issues and it allows for better control and upload status + recognition + final: true +- version: v1.0.18 + date: '2018-01-13' + changes: + - type: fix + text: Remove test data left in by mistake + final: true +- version: v1.0.17 + date: '2018-01-13' + changes: + - type: feature + text: Don't require restart for change of log level + - type: feature + text: Show status updates during update + - type: fix + text: In some cases restarting resulted in shutdown. If you are affected by this + you will to manually update the wrapper from this release + - type: fix + text: In some cases duplicate detection would throw an exception + - type: feature + text: Support JSON output for API searches + final: true +- version: v1.0.16 + date: '2018-01-11' + changes: + - type: fix + text: Make sure users don't enter an insane download limit value + - type: fix + text: Fix forbidden regexes which might've let some results through + - type: feature + text: Add option to disable CSRF protection and disable it by default + final: true +- version: v1.0.15 + date: '2018-01-10' + changes: + - type: feature + text: Pull NZB download status from configured downloaders instead of relying + on extension scripts + - type: feature + text: Add button to check caps for all/all incomplete (yellow) indexers + - type: fix + text: Anonymize username:password pairs in URLs in logs + - type: fix + text: Torznab results were returned wrong, preventing Hydra from being added to + radarr + final: true +- version: v1.0.14 + date: '2018-01-09' + changes: + - type: fix + text: Gracefully shutdown when restarting or quitting while search requests are + handled + final: true +- version: v1.0.13 + date: '2018-01-09' + changes: + - type: fix + text: NZBs proxied from indexers were returned with wrong / random seeming file + name + final: true +- version: v1.0.12 + date: '2018-01-07' + changes: + - type: feature + text: Allow migrating only the config, skipping the database migration + final: true +- version: v1.0.11 + date: '2018-01-07' + changes: + - type: fix + text: Fix error in auth introduced in a previous version + final: true +- version: v1.0.10 + date: '2018-01-07' + changes: + - type: feature + text: Improve the logging for web exceptions (which are often swallowed which + makes debugging harder) + - type: fix + text: Name of the category would not update in the category dropdown box on the + search page + - type: fix + text: Allow searching without a query in the UI + final: true +- version: v1.0.9 + date: '2018-01-07' + changes: + - type: fix + text: Allow NZBHydra2 to be shown in an iFrame (e.g. organizr) + final: true +- version: v1.0.8 + date: '2018-01-06' + changes: + - type: fix + text: Increase lengths for columns which may contain very long texts (errors, + queries) + final: true +- version: v1.0.7 + date: '2018-01-06' + changes: + - type: fix + text: Fix bug in wrapper that I introduced in last version. Oh well... + final: true +- version: v1.0.6 + date: '2018-01-06' + changes: + - type: note + text: Improve the way the host is determined. External URL should not need to + be set when not using a reverse proxy + - type: note + text: Remove PyYAML dependency from wrapper + final: true +- version: v1.0.5 + date: '2018-01-06' + changes: + - type: note + text: Make migration a bit more stable + - type: note + text: Make sure wrapper is started from correct folder + final: true +- version: v1.0.4 + date: '2018-01-06' + changes: + - type: note + text: So many fixes + final: true +- version: v1.0.3 + date: '2018-01-06' + changes: + - type: note + text: So many fixes + final: true +- version: v1.0.2 + date: '2018-01-06' + changes: + - type: note + text: First public release. Welcome! + final: true diff --git a/core/src/main/resources/config/application-build.properties b/core/src/main/resources/config/application-build.properties new file mode 100644 index 000000000..271234b09 --- /dev/null +++ b/core/src/main/resources/config/application-build.properties @@ -0,0 +1,4 @@ +main.useCsrf=false +nzbhydra.changelogUrl=http://mockserver:5080/changelog +nzbhydra.repositoryBaseUrl=http://mockserver:5080/repos/theotherp/nzbhydra2 +nzbhydra.newsUrl=http://mockserver:5080/static/news.json diff --git a/core/src/main/resources/config/application-systemtest.properties b/core/src/main/resources/config/application-systemtest.properties new file mode 100644 index 000000000..7e016dfcb --- /dev/null +++ b/core/src/main/resources/config/application-systemtest.properties @@ -0,0 +1 @@ +main.useCsrf=false diff --git a/core/src/main/resources/config/application.properties b/core/src/main/resources/config/application.properties index e18bbabdd..e18dc8e50 100644 --- a/core/src/main/resources/config/application.properties +++ b/core/src/main/resources/config/application.properties @@ -23,12 +23,13 @@ spring.profiles.active=default #Database connection, hibernate config -spring.datasource.url=jdbc:h2:file:${nzbhydra.dataFolder:.}/database/nzbhydra;MAX_COMPACT_TIME=${main.databaseCompactTime:15000};WRITE_DELAY=${main.databaseWriteDelay:5000};TRACE_MAX_FILE_SIZE=16;RETENTION_TIME=${main.databaseRetentionTime:1000} -spring.datasource.jdbc-url=jdbc:h2:file:${nzbhydra.dataFolder:.}/database/nzbhydra;MAX_COMPACT_TIME=${main.databaseCompactTime:15000};WRITE_DELAY=${main.databaseWriteDelay:5000};TRACE_MAX_FILE_SIZE=16;RETENTION_TIME=${main.databaseRetentionTime:1000} +spring.datasource.url=jdbc:h2:file:${nzbhydra.dataFolder:.}/database/nzbhydra;MAX_COMPACT_TIME=${main.databaseCompactTime:15000};WRITE_DELAY=${main.databaseWriteDelay:5000};TRACE_MAX_FILE_SIZE=16;RETENTION_TIME=${main.databaseRetentionTime:1000};NON_KEYWORDS=YEAR,DATA,KEY +spring.datasource.jdbc-url=jdbc:h2:file:${nzbhydra.dataFolder:.}/database/nzbhydra;MAX_COMPACT_TIME=${main.databaseCompactTime:15000};WRITE_DELAY=${main.databaseWriteDelay:5000};TRACE_MAX_FILE_SIZE=16;RETENTION_TIME=${main.databaseRetentionTime:1000};NON_KEYWORDS=YEAR,DATA,KEY spring.datasource.username=sa -spring.datasource.password= +spring.datasource.password=sa spring.datasource.driver-class-name=org.h2.Driver -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.database-platform=org.nzbhydra.database.H2DialectExtended +spring.jpa.properties.hibernate.dialect=org.nzbhydra.database.H2DialectExtended spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.use_sql_comments=false @@ -38,14 +39,14 @@ spring.jpa.properties.hibernate.cache.use_second_level_cache=false spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.order_updates=true spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true -spring.jpa.hibernate.use-new-id-generator-mappings=true spring.jpa.open-in-view=false +spring.h2.console.enabled=false #Migration spring.flyway.enabled=true -spring.flyway.table=schema_version spring.flyway.locations=classpath:/migration,classpath:/org/nzbhydra/database/migration spring.flyway.schemas=PUBLIC +#spring.flyway.baseline-on-migrate=true #spring.flyway.ignore-future-migrations=false #Jackson stuff @@ -117,7 +118,6 @@ management.endpoint.health.show-details=always management.endpoint.health.show-components=always management.endpoints.web.exposure.include=* -spring.jpa.hibernate.ddl-auto=validate spring.security.ignored=/static/** spring.security.headers.cache=false #spring.resources.chain.html-application-cache=true @@ -126,7 +126,7 @@ logging.config=classpath:config/logback.xml #Update settings nzbhydra.repositoryBaseUrl=https://api.github.com/repos/theotherp/nzbhydra2 -nzbhydra.changelogUrl=https://raw.githubusercontent.com/theotherp/nzbhydra2/master/core/src/main/resources/changelog.json +nzbhydra.changelogUrl=https://raw.githubusercontent.com/theotherp/nzbhydra2/master/core/src/main/resources/changelog.yaml nzbhydra.newsUrl=https://raw.githubusercontent.com/theotherp/nzbhydra2/master/news.json nzbhydra.blockedVersionsUrl=https://raw.githubusercontent.com/theotherp/nzbhydra2/master/blockedVersions.json #nzbhydra.repositoryBaseUrl=http://192.168.1.111:5080/repos/theotherp/nzbhydra2 diff --git a/core/src/main/resources/config/baseConfig.yml b/core/src/main/resources/config/baseConfig.yml index a3ca40b20..13a994d2a 100644 --- a/core/src/main/resources/config/baseConfig.yml +++ b/core/src/main/resources/config/baseConfig.yml @@ -260,14 +260,16 @@ categoriesConfig: searchType: SEARCH subtype: NONE downloading: - downloaders: [] - nzbAccessType: "PROXY" - saveTorrentsTo: null - saveNzbsTo: null - sendMagnetLinks: true - updateStatuses: true - showDownloaderStatus: true - fallbackForFailed: BOTH + downloaders: [ ] + nzbAccessType: "PROXY" + saveTorrentsTo: null + saveNzbsTo: null + sendMagnetLinks: true + updateStatuses: true + showDownloaderStatus: true + fallbackForFailed: BOTH + primaryDownloader: null + externalUrl: null genericStorage: {} indexers: [] main: @@ -326,18 +328,20 @@ main: welcomeShown: false xmx: 256 notificationConfig: - appriseApiUrl: null - entries: [ ] - filterOuts: [ ] - displayNotifications: true - displayNotificationsMax: 5 + appriseApiUrl: null + appriseCliPath: null + appriseType: "NONE" + entries: [ ] + filterOuts: [ ] + displayNotifications: true + displayNotificationsMax: 5 searching: alwaysConvertIds: "NONE" alwaysShowQuickFilterButtons: false applyRestrictions: "NONE" coverSize: 128 customQuickFilterButtons: [] - customQueryMappings: [ ] + customMappings: [ ] duplicateAgeThreshold: 2.0 duplicateSizeThresholdInPercent: 1.0 forbiddenGroups: [] @@ -358,6 +362,7 @@ searching: loadLimitInternal: 100 maxAge: null minSeeders: null + preselectQuickFilterButtons: [ ] removeTrailing: [ ".mp4", ".mkv", ".subs", ".REPOST", "repost", "~DG~", ".DG", "-DG", "-1", ".1", "(1)", "ReUp", "ReUp2", "-RP", "-AsRequested", "-Obfuscated", "-Scrambled", "-Chamele0n", "-BUYMORE", "-[TRP]", "-DG", ".par2", ".part01", "part01.rar", ".part02.rar", ".jpg", "[rartv]", "[rarbg]", "[eztv]", "English", "Korean", "Spanish", "French", "German", "Italian", "Danish", "Dutch", "Japanese", "Cantonese", "Mandarin", "Russian", "Polish", "Vietnamese", "Swedish", "Norwegian", "Finnish", "Turkish", "Portuguese", "Flemish", "Greek", "Hungarian", "-xpost" ] replaceUmlauts: false requiredRegex: null diff --git a/core/src/main/resources/config/logback.xml b/core/src/main/resources/config/logback.xml index b1e5658d3..4ecb0e8e8 100644 --- a/core/src/main/resources/config/logback.xml +++ b/core/src/main/resources/config/logback.xml @@ -34,16 +34,18 @@ org.springframework.security.web.csrf.CsrfFilter, org.springframework.security.web.header.HeaderWriterFilter, org.springframework.security.web.servletapi, - org.springframework.security.web.context.SecurityContextPersistenceFilter + org.springframework.security.web.context.SecurityContextPersistenceFilter, + org.springframework.security.web.context.SecurityContextHolderFilter, org.springframework.security.web.savedrequest, org.springframework.security.web.authentication.AnonymousAuthenticationFilter, + org.springframework.security.web.ObservationFilterChainDecorator, org.springframework.web.filter, org.springframework.cglib, org.springframework.aop, org.springframework.web.servlet, org.springframework.web.method, java.lang.reflect.Method, - javax.servlet, + jakarta.servlet, sun.reflect }}}"/> @@ -113,10 +117,10 @@ - + - + diff --git a/core/src/main/resources/migration/V1.0__INITIAL.sql b/core/src/main/resources/migration/V1.0__INITIAL.sql deleted file mode 100644 index c2aa0d65b..000000000 --- a/core/src/main/resources/migration/V1.0__INITIAL.sql +++ /dev/null @@ -1,181 +0,0 @@ -CREATE TABLE INDEXERSTATUS -( - ID INTEGER PRIMARY KEY NOT NULL, - DISABLED_PERMANENTLY BOOLEAN, - DISABLED_UNTIL TIMESTAMP, - FIRST_FAILURE TIMESTAMP, - LAST_FAILURE TIMESTAMP, - LEVEL INTEGER, - REASON VARCHAR(4000) -); - -CREATE TABLE MOVIEINFO -( - ID INTEGER PRIMARY KEY NOT NULL, - IMDB_ID VARCHAR(255), - POSTER_URL VARCHAR(255), - TITLE VARCHAR(255), - TMDB_ID VARCHAR(255), - YEAR INTEGER -); - -CREATE TABLE SEARCH -( - ID INTEGER PRIMARY KEY NOT NULL, - AUTHOR VARCHAR(255), - CATEGORY_NAME VARCHAR(255), - EPISODE VARCHAR(255), - QUERY VARCHAR(255), - SEARCH_TYPE VARCHAR(255), - SEASON INTEGER, - SOURCE VARCHAR(255), - TIME TIMESTAMP, - TITLE VARCHAR(255), - USER_AGENT VARCHAR(255), - USERNAME_OR_IP VARCHAR(255) -); - -CREATE TABLE GENERIC_STORAGE_DATA -( - ID INTEGER PRIMARY KEY NOT NULL, - DATA VARCHAR(255), - KEY VARCHAR(255) -); - -CREATE TABLE IDENTIFIER_KEY_VALUE_PAIR -( - ID INTEGER PRIMARY KEY NOT NULL, - IDENTIFIER_KEY VARCHAR(255), - IDENTIFIER_VALUE VARCHAR(255) -); - -CREATE TABLE INDEXER -( - ID INTEGER PRIMARY KEY NOT NULL, - NAME VARCHAR(255), - STATUS_ID INTEGER, - CONSTRAINT FKI97W3VAGIEAICA0TQ0WXNHYB2 FOREIGN KEY (STATUS_ID) REFERENCES INDEXERSTATUS (ID) -); -CREATE UNIQUE INDEX UK_XIFS7FHUVN4UB7IGF11FQEP0_INDEX_9 - ON INDEXER (NAME); - -CREATE TABLE INDEXERAPIACCESS -( - ID INTEGER PRIMARY KEY NOT NULL, - ACCESS_TYPE VARCHAR(255), - ERROR VARCHAR(4000), - RESPONSE_TIME BIGINT, - RESULT VARCHAR(255), - TIME TIMESTAMP, - INDEXER_ID INTEGER, - CONSTRAINT FKFLRMBYQ8CLDV48SWRYIY0YJD2 FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) -); - -CREATE INDEX INDEXERAPIACC_TIME_index ON INDEXERAPIACCESS (TIME); -CREATE INDEX INDEXERAPIACC_INDID_TIME_index ON INDEXERAPIACCESS (INDEXER_ID, TIME); - -CREATE TABLE INDEXERAPIACCESS_SHORT -( - ID INTEGER PRIMARY KEY NOT NULL, - INDEXER_ID INTEGER, - TIME TIMESTAMP, - SUCCESSFUL BOOLEAN, - CONSTRAINT FKFLRMBYQ8CLDV48KDUNDZHD FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) -); -CREATE INDEX INDEXERAPIACCESS_SHORT_INDEXER_ID_TIME_INDEX - ON INDEXERAPIACCESS_SHORT (INDEXER_ID, TIME DESC); - -CREATE TABLE INDEXERNZBDOWNLOAD -( - ID INTEGER PRIMARY KEY NOT NULL, - ACCESS_SOURCE VARCHAR(255), - AGE INTEGER, - ERROR VARCHAR(255), - EXTERNAL_ID VARCHAR(255), - NZB_ACCESS_TYPE VARCHAR(255), - STATUS VARCHAR(255), - TIME TIMESTAMP, - TITLE VARCHAR(4000), - USERNAME_OR_IP VARCHAR(255), - INDEXER_ID INTEGER, - CONSTRAINT FKMTRRF4HK98C9O3FDQJTS3HUPB FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) -); -CREATE INDEX NZB_DOWNLOAD_EXT_ID - ON INDEXERNZBDOWNLOAD (EXTERNAL_ID); -CREATE INDEX INDEXERNZBDOWNLOAD_INDEXER_ID_TIME_INDEX - ON INDEXERNZBDOWNLOAD (INDEXER_ID, TIME DESC); - -CREATE TABLE INDEXERSEARCH -( - ID INTEGER PRIMARY KEY NOT NULL, - PROCESSED_RESULTS INTEGER, - RESULTS_COUNT INTEGER, - SUCCESSFUL BOOLEAN, - UNIQUE_RESULTS INTEGER, - INDEXER_ENTITY_ID INTEGER, - SEARCH_ENTITY_ID INTEGER, - CONSTRAINT FK48A7TLYKV21V8CEGF6SUCDYGX FOREIGN KEY (INDEXER_ENTITY_ID) REFERENCES INDEXER (ID), - CONSTRAINT FKOBOGIXH7OUOCYK1M0417GYQKR FOREIGN KEY (SEARCH_ENTITY_ID) REFERENCES SEARCH (ID) -); -CREATE INDEX INDEXERSEARCH_INDEXER_ENTITY_ID_SEARCH_ENTITY_ID_INDEX - ON INDEXERSEARCH (INDEXER_ENTITY_ID, SEARCH_ENTITY_ID); - - - -CREATE TABLE PERSISTENT_LOGINS -( - SERIES VARCHAR(255) PRIMARY KEY NOT NULL, - LAST_USED TIMESTAMP NOT NULL, - TOKEN VARCHAR(255) NOT NULL, - USERNAME VARCHAR(255) NOT NULL -); - -CREATE TABLE SEARCHRESULT -( - ID BIGINT PRIMARY KEY NOT NULL, - DETAILS VARCHAR(4000), - DOWNLOAD_TYPE VARCHAR(255), - FIRST_FOUND TIMESTAMP, - INDEXERGUID VARCHAR(255) NOT NULL, - LINK VARCHAR(4000), - PUB_DATE TIMESTAMP, - TITLE VARCHAR(4000) NOT NULL, - INDEXER_ID INTEGER NOT NULL, - CONSTRAINT FKR5G21PDW3HHS1SEFVJY30TGMI FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) -); -CREATE UNIQUE INDEX UKFTFA80663URIMM78EPNXHYOM_INDEX_C - ON SEARCHRESULT (INDEXER_ID, INDEXERGUID); - -CREATE TABLE SEARCH_IDENTIFIERS -( - SEARCH_ENTITY_ID INTEGER NOT NULL, - IDENTIFIERS_ID INTEGER NOT NULL, - CONSTRAINT CONSTRAINT_66 PRIMARY KEY (SEARCH_ENTITY_ID, IDENTIFIERS_ID), - CONSTRAINT FKG7116YD6U2Q5LPQQLCLDKBVGL FOREIGN KEY (SEARCH_ENTITY_ID) REFERENCES SEARCH (ID), - CONSTRAINT FK8V41HNWG7RV1GELG37QK9M7WK FOREIGN KEY (IDENTIFIERS_ID) REFERENCES IDENTIFIER_KEY_VALUE_PAIR (ID) -); - -CREATE TABLE SHOWNNEWS -( - ID INTEGER PRIMARY KEY NOT NULL, - VERSION VARCHAR(255) -); - -CREATE TABLE TVINFO -( - ID INTEGER PRIMARY KEY NOT NULL, - POSTER_URL VARCHAR(255), - TITLE VARCHAR(255), - TVDB_ID VARCHAR(255), - TVMAZE_ID VARCHAR(255), - TVRAGE_ID VARCHAR(255), - YEAR INTEGER -); -CREATE UNIQUE INDEX UK_PGYJIFSNJSVJ1W9P0XVIDGP5E_INDEX_9 - ON TVINFO (TVDB_ID); -CREATE UNIQUE INDEX UK_NJKRL57AGU954UJKOTT65HWHH_INDEX_9 - ON TVINFO (TVMAZE_ID); -CREATE UNIQUE INDEX UK_GFWLXF98S7J77CF7G6FSFVSS0_INDEX_9 - ON TVINFO (TVRAGE_ID); - -CREATE SEQUENCE PUBLIC.hibernate_sequence; diff --git a/core/src/main/resources/migration/V1.10__ADD_INDEX_FOR_DOWNLOAD_STATUS_UPDATE_QUERIES.sql b/core/src/main/resources/migration/V1.10__ADD_INDEX_FOR_DOWNLOAD_STATUS_UPDATE_QUERIES.sql deleted file mode 100644 index 61c76c1b2..000000000 --- a/core/src/main/resources/migration/V1.10__ADD_INDEX_FOR_DOWNLOAD_STATUS_UPDATE_QUERIES.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX INDEXERNZBDOWNLOAD_STATUS_INDEX ON INDEXERNZBDOWNLOAD (STATUS, TIME DESC ); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.11__REMOVE_INDEXERSTATUSES.sql b/core/src/main/resources/migration/V1.11__REMOVE_INDEXERSTATUSES.sql deleted file mode 100644 index 60a7c205a..000000000 --- a/core/src/main/resources/migration/V1.11__REMOVE_INDEXERSTATUSES.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE INDEXERSTATUS - DROP CONSTRAINT IF EXISTS FKI97W3VAGIEAICA0TQ0WXNHYB2; -DROP TABLE INDEXERSTATUS; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.12__ADD_MEDIAINFO_UNIQUE.sql b/core/src/main/resources/migration/V1.12__ADD_MEDIAINFO_UNIQUE.sql deleted file mode 100644 index d5ac0d6ac..000000000 --- a/core/src/main/resources/migration/V1.12__ADD_MEDIAINFO_UNIQUE.sql +++ /dev/null @@ -1,4 +0,0 @@ ---Removing duplicates is hard, so we'll just delete the existing ones -DELETE FROM MOVIEINFO; -ALTER TABLE MOVIEINFO - ADD CONSTRAINT MOVIEINFO_TMDB_ID_IMDB_ID_pk UNIQUE (TMDB_ID, IMDB_ID); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.13__EXTEND_GENERICSTORAGE_DATA_LENGTH.sql b/core/src/main/resources/migration/V1.13__EXTEND_GENERICSTORAGE_DATA_LENGTH.sql deleted file mode 100644 index 3e9a6a8c0..000000000 --- a/core/src/main/resources/migration/V1.13__EXTEND_GENERICSTORAGE_DATA_LENGTH.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE GENERIC_STORAGE_DATA - ALTER COLUMN DATA CLOB; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.14__SEARCHRESULT_LINK_LENGTH.sql b/core/src/main/resources/migration/V1.14__SEARCHRESULT_LINK_LENGTH.sql deleted file mode 100644 index 5d7de5c10..000000000 --- a/core/src/main/resources/migration/V1.14__SEARCHRESULT_LINK_LENGTH.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE SEARCHRESULT - ALTER COLUMN LINK varchar; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.17__SHORTACCESS_AGAIN.sql b/core/src/main/resources/migration/V1.17__SHORTACCESS_AGAIN.sql deleted file mode 100644 index 1b2ae134e..000000000 --- a/core/src/main/resources/migration/V1.17__SHORTACCESS_AGAIN.sql +++ /dev/null @@ -1,28 +0,0 @@ -DROP TABLE INDEXERAPIACCESS_SHORT; - -CREATE TABLE INDEXERAPIACCESS_SHORT -( - ID INTEGER PRIMARY KEY NOT NULL, - INDEXER_ID INTEGER, - TIME TIMESTAMP, - SUCCESSFUL BOOLEAN, - API_ACCESS_TYPE VARCHAR2(255), - CONSTRAINT FKFLRMBYQ8CLDV48KDUNDZHD FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) -); - - -CREATE INDEX INDEXERAPIACCESS_SHORT_ACCESSTYPE_INDEXER_ID_TIME_INDEX - ON INDEXERAPIACCESS_SHORT (INDEXER_ID, API_ACCESS_TYPE, TIME DESC); - - -insert into INDEXERAPIACCESS_SHORT (ID, INDEXER_ID, TIME, SUCCESSFUL, API_ACCESS_TYPE) -SELECT HIBERNATE_SEQUENCE.Nextval, x.INDEXER_ID, x.TIME, true, 'SEARCH' -FROM (select A.TIME, A.INDEXER_ID from INDEXERAPIACCESS a where a.TIME > DATEADD('DAY', -2, CURRENT_TIMESTAMP())) x; - - -insert into INDEXERAPIACCESS_SHORT (ID, INDEXER_ID, TIME, SUCCESSFUL, API_ACCESS_TYPE) -SELECT HIBERNATE_SEQUENCE.Nextval, x.INDEXER_ID, x.TIME, true, 'NZB' -FROM (select D.TIME, R.INDEXER_ID - from INDEXERNZBDOWNLOAD D - left join SEARCHRESULT R on D.ID = R.ID - where d.TIME > DATEADD('DAY', -2, CURRENT_TIMESTAMP())) x; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.18__SEARCH_HISTORY_INDEXES.sql b/core/src/main/resources/migration/V1.18__SEARCH_HISTORY_INDEXES.sql deleted file mode 100644 index d27c58383..000000000 --- a/core/src/main/resources/migration/V1.18__SEARCH_HISTORY_INDEXES.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE INDEX SEARCH_USER_HISTORY_INDEX1 - ON SEARCH (USERNAME, SOURCE, TIME DESC); -CREATE INDEX SEARCH_USER_HISTORY_INDEX2 - ON SEARCH (SOURCE, TIME DESC); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.19__ADD_TVINFO_IMDB_COLUMN.sql b/core/src/main/resources/migration/V1.19__ADD_TVINFO_IMDB_COLUMN.sql deleted file mode 100644 index ba568a064..000000000 --- a/core/src/main/resources/migration/V1.19__ADD_TVINFO_IMDB_COLUMN.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE NZBHYDRA.PUBLIC.TVINFO - ADD IMDB_ID VARCHAR(255); diff --git a/core/src/main/resources/migration/V1.1__NZBDOWNLOAD_USERAGENT.sql b/core/src/main/resources/migration/V1.1__NZBDOWNLOAD_USERAGENT.sql deleted file mode 100644 index 89756ef3d..000000000 --- a/core/src/main/resources/migration/V1.1__NZBDOWNLOAD_USERAGENT.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE INDEXERNZBDOWNLOAD ADD USER_AGENT VARCHAR(4000); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.20__EMPTY_TV_AND_MOVIE_INFO.sql b/core/src/main/resources/migration/V1.20__EMPTY_TV_AND_MOVIE_INFO.sql deleted file mode 100644 index d73b09ba2..000000000 --- a/core/src/main/resources/migration/V1.20__EMPTY_TV_AND_MOVIE_INFO.sql +++ /dev/null @@ -1,5 +0,0 @@ ---Make sure no info entries with IMDB IDs without leading tt exist -DELETE -FROM NZBHYDRA.PUBLIC.TVINFO; -DELETE -FROM NZBHYDRA.PUBLIC.MOVIEINFO; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.21__LINK_SEARCHENTITY_TO_INDEXERSEARCHENTITY.sql b/core/src/main/resources/migration/V1.21__LINK_SEARCHENTITY_TO_INDEXERSEARCHENTITY.sql deleted file mode 100644 index 937064a82..000000000 --- a/core/src/main/resources/migration/V1.21__LINK_SEARCHENTITY_TO_INDEXERSEARCHENTITY.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE SEARCHRESULT - ADD COLUMN IF NOT EXISTS indexerSearchEntity INTEGER; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.22__INDEXERUNIQUENESSSCORE.sql b/core/src/main/resources/migration/V1.22__INDEXERUNIQUENESSSCORE.sql deleted file mode 100644 index cdf4ffb5c..000000000 --- a/core/src/main/resources/migration/V1.22__INDEXERUNIQUENESSSCORE.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE IF NOT EXISTS INDEXERUNIQUENESSSCORE -( - ID INTEGER PRIMARY KEY NOT NULL, - INDEXER_ID INTEGER NOT NULL, - INVOLVED INTEGER NOT NULL, - HAVE INTEGER NOT NULL, - HASRESULT BOOLEAN NOT NULL, - CONSTRAINT MFKFLRMBYQ8CLDV48SWRYIY0YKD2 FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) -); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.23__INDEXERUNIQUENESSSCORE_DELETEOLD.sql b/core/src/main/resources/migration/V1.23__INDEXERUNIQUENESSSCORE_DELETEOLD.sql deleted file mode 100644 index e7b4a1342..000000000 --- a/core/src/main/resources/migration/V1.23__INDEXERUNIQUENESSSCORE_DELETEOLD.sql +++ /dev/null @@ -1,2 +0,0 @@ -DELETE -FROM INDEXERUNIQUENESSSCORE; diff --git a/core/src/main/resources/migration/V1.24__REMOVE_OLD_SCORE_COLUMNS.sql b/core/src/main/resources/migration/V1.24__REMOVE_OLD_SCORE_COLUMNS.sql deleted file mode 100644 index f4a6cf1da..000000000 --- a/core/src/main/resources/migration/V1.24__REMOVE_OLD_SCORE_COLUMNS.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE INDEXERSEARCH - DROP COLUMN IF EXISTS PROCESSED_RESULTS; -ALTER TABLE INDEXERSEARCH - DROP COLUMN IF EXISTS UNIQUE_RESULTS; diff --git a/core/src/main/resources/migration/V1.25__INDEXERSTATUS.sql b/core/src/main/resources/migration/V1.25__INDEXERSTATUS.sql deleted file mode 100644 index 4ffc98b79..000000000 --- a/core/src/main/resources/migration/V1.25__INDEXERSTATUS.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE INDEXERLIMIT -( - ID INTEGER PRIMARY KEY NOT NULL, - INDEXER_ID INTEGER, - API_HITS INTEGER, - API_HIT_LIMIT INTEGER, - DOWNLOADS INTEGER, - DOWNLOAD_LIMIT INTEGER, - OLDEST_API_HIT TIMESTAMP, - OLDEST_DOWNLOAD TIMESTAMP, - CONSTRAINT DABCDLRMBYQ8CLDV48SWRYIY0YKD2 FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) -); diff --git a/core/src/main/resources/migration/V1.2__DISTINGUISH_USERNAME_IP.sql b/core/src/main/resources/migration/V1.2__DISTINGUISH_USERNAME_IP.sql deleted file mode 100644 index ad446d453..000000000 --- a/core/src/main/resources/migration/V1.2__DISTINGUISH_USERNAME_IP.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE SEARCH ADD IP VARCHAR(255); -ALTER TABLE INDEXERNZBDOWNLOAD ADD IP VARCHAR(255); - -ALTER TABLE SEARCH ALTER COLUMN USERNAME_OR_IP RENAME TO USERNAME; -ALTER TABLE INDEXERNZBDOWNLOAD ALTER COLUMN USERNAME_OR_IP RENAME TO USERNAME; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.3__DELETE_OLD_SEARCHRESULTS.sql b/core/src/main/resources/migration/V1.3__DELETE_OLD_SEARCHRESULTS.sql deleted file mode 100644 index 24b0661d4..000000000 --- a/core/src/main/resources/migration/V1.3__DELETE_OLD_SEARCHRESULTS.sql +++ /dev/null @@ -1,2 +0,0 @@ ---Delete search results with old ID calculation method -DELETE FROM SEARCHRESULT; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.4__MAKE_SEARCHRESULT_INDEX_NON_UNIQUE.sql b/core/src/main/resources/migration/V1.4__MAKE_SEARCHRESULT_INDEX_NON_UNIQUE.sql deleted file mode 100644 index 791e8e58e..000000000 --- a/core/src/main/resources/migration/V1.4__MAKE_SEARCHRESULT_INDEX_NON_UNIQUE.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX UKFTFA80663URIMM78EPNXHYOM_INDEX_C; -CREATE INDEX UKFTFA80663URIMM78EPNXHYOM_INDEX_C ON SEARCHRESULT (INDEXER_ID, INDEXERGUID); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.5__MAKE_SEARCH_QUERY_COLUMN_BIGGER.sql b/core/src/main/resources/migration/V1.5__MAKE_SEARCH_QUERY_COLUMN_BIGGER.sql deleted file mode 100644 index 2ef4a2eb4..000000000 --- a/core/src/main/resources/migration/V1.5__MAKE_SEARCH_QUERY_COLUMN_BIGGER.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE SEARCH - ALTER COLUMN QUERY VARCHAR2(4000); - -ALTER TABLE SEARCHRESULT - ALTER COLUMN INDEXERGUID VARCHAR2(4000); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.6__ADD_SEARCH_RESULT_TO_DOWNLOADS.sql b/core/src/main/resources/migration/V1.6__ADD_SEARCH_RESULT_TO_DOWNLOADS.sql deleted file mode 100644 index c4e4141ea..000000000 --- a/core/src/main/resources/migration/V1.6__ADD_SEARCH_RESULT_TO_DOWNLOADS.sql +++ /dev/null @@ -1,17 +0,0 @@ -ALTER TABLE INDEXERNZBDOWNLOAD - ADD SEARCH_RESULT_ID BIGINT; - ---good enough -UPDATE INDEXERNZBDOWNLOAD d -SET d.SEARCH_RESULT_ID = (SELECT x.ID - FROM SEARCHRESULT x - WHERE d.title = x.TITLE - LIMIT 1); - -ALTER TABLE INDEXERNZBDOWNLOAD - DROP COLUMN TITLE; - -ALTER TABLE INDEXERNZBDOWNLOAD DROP CONSTRAINT FKMTRRF4HK98C9O3FDQJTS3HUPB; -DROP INDEX INDEXERNZBDOWNLOAD_INDEXER_ID_TIME_INDEX; -ALTER TABLE INDEXERNZBDOWNLOAD - DROP COLUMN INDEXER_ID; \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.7__ADD_INDEXES_FOR_STATS.sql b/core/src/main/resources/migration/V1.7__ADD_INDEXES_FOR_STATS.sql deleted file mode 100644 index e3a4b60e7..000000000 --- a/core/src/main/resources/migration/V1.7__ADD_INDEXES_FOR_STATS.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE INDEX INDEXERAPIACCESS_TIME_INDEX ON INDEXERAPIACCESS (TIME DESC); -CREATE INDEX INDEXERNZBDOWNLOAD_TIME_INDEX ON INDEXERNZBDOWNLOAD (TIME DESC); -CREATE INDEX INDEXERNZBDOWNLOAD_SEARCHRESULTID_INDEX ON INDEXERNZBDOWNLOAD (SEARCH_RESULT_ID); -CREATE INDEX SEARCH_TIME_INDEX ON SEARCH (TIME DESC); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.8__INCREASE_QUERY_AND_ERROR_COLUMN_LENGTHs.sql b/core/src/main/resources/migration/V1.8__INCREASE_QUERY_AND_ERROR_COLUMN_LENGTHs.sql deleted file mode 100644 index 41d184b82..000000000 --- a/core/src/main/resources/migration/V1.8__INCREASE_QUERY_AND_ERROR_COLUMN_LENGTHs.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE SEARCH ALTER COLUMN QUERY VARCHAR2(1000); -ALTER TABLE INDEXERNZBDOWNLOAD ALTER COLUMN ERROR VARCHAR2(4000); \ No newline at end of file diff --git a/core/src/main/resources/migration/V1.9__SET_DELETE_CASCADES.sql b/core/src/main/resources/migration/V1.9__SET_DELETE_CASCADES.sql deleted file mode 100644 index 438894156..000000000 --- a/core/src/main/resources/migration/V1.9__SET_DELETE_CASCADES.sql +++ /dev/null @@ -1,28 +0,0 @@ -ALTER TABLE INDEXERAPIACCESS DROP CONSTRAINT FKFLRMBYQ8CLDV48SWRYIY0YJD2; -ALTER TABLE INDEXERAPIACCESS - ADD CONSTRAINT FKFLRMBYQ8CLDV48SWRYIY0YJD2 -FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) ON DELETE CASCADE; - -ALTER TABLE INDEXERAPIACCESS_SHORT DROP CONSTRAINT FKFLRMBYQ8CLDV48KDUNDZHD; -ALTER TABLE INDEXERAPIACCESS_SHORT - ADD CONSTRAINT FKFLRMBYQ8CLDV48KDUNDZHD -FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) ON DELETE CASCADE; - -ALTER TABLE INDEXERSEARCH DROP CONSTRAINT FKOBOGIXH7OUOCYK1M0417GYQKR; -ALTER TABLE INDEXERSEARCH - ADD CONSTRAINT FKOBOGIXH7OUOCYK1M0417GYQKR -FOREIGN KEY (INDEXER_ENTITY_ID) REFERENCES INDEXER (ID) ON DELETE CASCADE; - -ALTER TABLE INDEXERSEARCH DROP CONSTRAINT FK48A7TLYKV21V8CEGF6SUCDYGX; -ALTER TABLE INDEXERSEARCH - ADD CONSTRAINT FK48A7TLYKV21V8CEGF6SUCDYGX -FOREIGN KEY (SEARCH_ENTITY_ID) REFERENCES SEARCH (ID) ON DELETE CASCADE; - -ALTER TABLE SEARCHRESULT DROP CONSTRAINT FKR5G21PDW3HHS1SEFVJY30TGMI; -ALTER TABLE SEARCHRESULT - ADD CONSTRAINT FKR5G21PDW3HHS1SEFVJY30TGMI -FOREIGN KEY (INDEXER_ID) REFERENCES INDEXER (ID) ON DELETE CASCADE; - -ALTER TABLE INDEXERNZBDOWNLOAD - ADD CONSTRAINT FKR5G21PDW3HHS1SEFKHD3HBDGL -FOREIGN KEY (SEARCH_RESULT_ID) REFERENCES SEARCHRESULT (ID) ON DELETE CASCADE; diff --git a/core/src/main/resources/migration/V1__INITIAL.sql b/core/src/main/resources/migration/V1__INITIAL.sql new file mode 100644 index 000000000..d16a2c59f --- /dev/null +++ b/core/src/main/resources/migration/V1__INITIAL.sql @@ -0,0 +1,271 @@ +create sequence HIBERNATE_SEQUENCE + start with 50; + + +create table GENERIC_STORAGE_DATA +( + ID INTEGER not null + primary key, + DATA CHARACTER LARGE OBJECT, + KEY CHARACTER VARYING(255) +); + +create table IDENTIFIER_KEY_VALUE_PAIR +( + ID INTEGER not null + primary key, + IDENTIFIER_KEY CHARACTER VARYING(255), + IDENTIFIER_VALUE CHARACTER VARYING(255) +); + +create table INDEXER +( + ID INTEGER not null + primary key, + NAME CHARACTER VARYING(255), + STATUS_ID INTEGER +); + +create unique index UK_XIFS7FHUVN4UB7IGF11FQEP0_INDEX_9 + on INDEXER (NAME); + +create table INDEXERAPIACCESS +( + ID INTEGER not null + primary key, + ACCESS_TYPE CHARACTER VARYING(255), + ERROR CHARACTER VARYING(4000), + RESPONSE_TIME BIGINT, + RESULT CHARACTER VARYING(255), + TIME TIMESTAMP, + INDEXER_ID INTEGER, + constraint FKFLRMBYQ8CLDV48SWRYIY0YJD2 + foreign key (INDEXER_ID) references INDEXER + on delete cascade +); + +create index INDEXERAPIACCESS_TIME_INDEX + on INDEXERAPIACCESS (TIME desc); + +create index INDEXERAPIACC_INDID_TIME_INDEX + on INDEXERAPIACCESS (INDEXER_ID, TIME); + +create index INDEXERAPIACC_TIME_INDEX + on INDEXERAPIACCESS (TIME); + +create table INDEXERAPIACCESS_SHORT +( + ID INTEGER not null + primary key, + INDEXER_ID INTEGER, + TIME TIMESTAMP, + SUCCESSFUL BOOLEAN, + API_ACCESS_TYPE CHARACTER VARYING(255), + constraint FKFLRMBYQ8CLDV48KDUNDZHD + foreign key (INDEXER_ID) references INDEXER +); + +create index INDEXERAPIACCESS_SHORT_ACCESSTYPE_INDEXER_ID_TIME_INDEX + on INDEXERAPIACCESS_SHORT (INDEXER_ID asc, API_ACCESS_TYPE asc, TIME desc); + +create table INDEXERLIMIT +( + ID INTEGER not null + primary key, + INDEXER_ID INTEGER, + API_HITS INTEGER, + API_HIT_LIMIT INTEGER, + DOWNLOADS INTEGER, + DOWNLOAD_LIMIT INTEGER, + OLDEST_API_HIT TIMESTAMP, + OLDEST_DOWNLOAD TIMESTAMP, + constraint DABCDLRMBYQ8CLDV48SWRYIY0YKD2 + foreign key (INDEXER_ID) references INDEXER +); + +create table INDEXERUNIQUENESSSCORE +( + ID INTEGER not null + primary key, + INDEXER_ID INTEGER not null, + INVOLVED INTEGER not null, + HAVE INTEGER not null, + HASRESULT BOOLEAN not null, + constraint MFKFLRMBYQ8CLDV48SWRYIY0YKD2 + foreign key (INDEXER_ID) references INDEXER +); + +create table MOVIEINFO +( + ID INTEGER not null + primary key, + IMDB_ID CHARACTER VARYING(255), + POSTER_URL CHARACTER VARYING(255), + TITLE CHARACTER VARYING(255), + TMDB_ID CHARACTER VARYING(255), + YEAR INTEGER, + constraint MOVIEINFO_TMDB_ID_IMDB_ID_PK + unique (TMDB_ID, IMDB_ID) +); + +create table NOTIFICATION +( + ID INTEGER not null + primary key, + NOTIFICATION_EVENT_TYPE CHARACTER VARYING(255) not null, + MESSAGE_TYPE CHARACTER VARYING(255) not null, + TITLE CHARACTER VARYING(255), + BODY CHARACTER VARYING(255) not null, + URLS CHARACTER VARYING(255), + TIME TIMESTAMP not null, + DISPLAYED BOOLEAN default FALSE not null +); + +create table PERSISTENT_LOGINS +( + SERIES CHARACTER VARYING(255) not null + primary key, + LAST_USED TIMESTAMP not null, + TOKEN CHARACTER VARYING(255) not null, + USERNAME CHARACTER VARYING(255) not null +); + +create table SEARCH +( + ID INTEGER not null + primary key, + AUTHOR CHARACTER VARYING(255), + CATEGORY_NAME CHARACTER VARYING(255), + EPISODE CHARACTER VARYING(255), + QUERY CHARACTER VARYING(1000), + SEARCH_TYPE CHARACTER VARYING(255), + SEASON INTEGER, + SOURCE CHARACTER VARYING(255), + TIME TIMESTAMP, + TITLE CHARACTER VARYING(255), + USER_AGENT CHARACTER VARYING(255), + USERNAME CHARACTER VARYING(255), + IP CHARACTER VARYING(255) +); + +create table INDEXERSEARCH +( + ID INTEGER not null + primary key, + RESULTS_COUNT INTEGER, + SUCCESSFUL BOOLEAN, + INDEXER_ENTITY_ID INTEGER, + SEARCH_ENTITY_ID INTEGER, + constraint FK48A7TLYKV21V8CEGF6SUCDYGX + foreign key (SEARCH_ENTITY_ID) references SEARCH + on delete cascade, + constraint FKOBOGIXH7OUOCYK1M0417GYQKR + foreign key (INDEXER_ENTITY_ID) references INDEXER + on delete cascade +); + +create index INDEXERSEARCH_INDEXER_ENTITY_ID_SEARCH_ENTITY_ID_INDEX + on INDEXERSEARCH (INDEXER_ENTITY_ID, SEARCH_ENTITY_ID); + +create index SEARCH_TIME_INDEX + on SEARCH (TIME desc); + +create index SEARCH_USER_HISTORY_INDEX1 + on SEARCH (USERNAME asc, SOURCE asc, TIME desc); + +create index SEARCH_USER_HISTORY_INDEX2 + on SEARCH (SOURCE asc, TIME desc); + +create table SEARCHRESULT +( + ID BIGINT not null + primary key, + DETAILS CHARACTER VARYING(4000), + DOWNLOAD_TYPE CHARACTER VARYING(255), + FIRST_FOUND TIMESTAMP, + INDEXERGUID CHARACTER VARYING(4000) not null, + LINK CHARACTER VARYING, + PUB_DATE TIMESTAMP, + TITLE CHARACTER VARYING(4000) not null, + INDEXER_ID INTEGER not null, + INDEXERSEARCHENTITY INTEGER, + constraint FKR5G21PDW3HHS1SEFVJY30TGMI + foreign key (INDEXER_ID) references INDEXER + on delete cascade +); + +create table INDEXERNZBDOWNLOAD +( + ID INTEGER not null + primary key, + ACCESS_SOURCE CHARACTER VARYING(255), + AGE INTEGER, + ERROR CHARACTER VARYING(4000), + EXTERNAL_ID CHARACTER VARYING(255), + NZB_ACCESS_TYPE CHARACTER VARYING(255), + STATUS CHARACTER VARYING(255), + TIME TIMESTAMP, + USERNAME CHARACTER VARYING(255), + USER_AGENT CHARACTER VARYING(4000), + IP CHARACTER VARYING(255), + SEARCH_RESULT_ID BIGINT, + constraint FKR5G21PDW3HHS1SEFKHD3HBDGL + foreign key (SEARCH_RESULT_ID) references SEARCHRESULT + on delete cascade +); + +create index INDEXERNZBDOWNLOAD_SEARCHRESULTID_INDEX + on INDEXERNZBDOWNLOAD (SEARCH_RESULT_ID); + +create index INDEXERNZBDOWNLOAD_STATUS_INDEX + on INDEXERNZBDOWNLOAD (STATUS asc, TIME desc); + +create index INDEXERNZBDOWNLOAD_TIME_INDEX + on INDEXERNZBDOWNLOAD (TIME desc); + +create index NZB_DOWNLOAD_EXT_ID + on INDEXERNZBDOWNLOAD (EXTERNAL_ID); + +create index UKFTFA80663URIMM78EPNXHYOM_INDEX_C + on SEARCHRESULT (INDEXER_ID, INDEXERGUID); + +create table SEARCH_IDENTIFIERS +( + SEARCH_ENTITY_ID INTEGER not null, + IDENTIFIERS_ID INTEGER not null, + primary key (SEARCH_ENTITY_ID, IDENTIFIERS_ID), + constraint FK8V41HNWG7RV1GELG37QK9M7WK + foreign key (IDENTIFIERS_ID) references IDENTIFIER_KEY_VALUE_PAIR, + constraint FKG7116YD6U2Q5LPQQLCLDKBVGL + foreign key (SEARCH_ENTITY_ID) references SEARCH +); + +create table SHOWNNEWS +( + ID INTEGER not null + primary key, + VERSION CHARACTER VARYING(255) +); + +create table TVINFO +( + ID INTEGER not null + primary key, + POSTER_URL CHARACTER VARYING(255), + TITLE CHARACTER VARYING(255), + TVDB_ID CHARACTER VARYING(255), + TVMAZE_ID CHARACTER VARYING(255), + TVRAGE_ID CHARACTER VARYING(255), + YEAR INTEGER, + IMDB_ID CHARACTER VARYING(255) +); + +create unique index UK_GFWLXF98S7J77CF7G6FSFVSS0_INDEX_9 + on TVINFO (TVRAGE_ID); + +create unique index UK_NJKRL57AGU954UJKOTT65HWHH_INDEX_9 + on TVINFO (TVMAZE_ID); + +create unique index UK_PGYJIFSNJSVJ1W9P0XVIDGP5E_INDEX_9 + on TVINFO (TVDB_ID); + diff --git a/core/src/main/resources/migration/V2__SEQUENCES.SQL b/core/src/main/resources/migration/V2__SEQUENCES.SQL new file mode 100644 index 000000000..b1ab48d17 --- /dev/null +++ b/core/src/main/resources/migration/V2__SEQUENCES.SQL @@ -0,0 +1,54 @@ +--v1 did not contain explicit sequences for the autogenerated IDs. We start with a reasonably high initial value +--Hibernate uses an increment of 50 by default and I was unable to change that + +create sequence IDENTIFIER_KEY_VALUE_PAIR_SEQ + increment by 50 + start with 30000000000; + +create sequence INDEXERAPIACCESS_SEQ + increment by 50 + start with 30000000000; + +create sequence INDEXERAPIACCESS_SHORT_SEQ + increment by 50 + start with 30000000000; + +create sequence INDEXERLIMIT_SEQ + increment by 50 + start with 30000000000; + +create sequence INDEXERNZBDOWNLOAD_SEQ + increment by 50 + start with 30000000000; + +create sequence INDEXERSEARCH_SEQ + increment by 50 + start with 30000000000; + +create sequence INDEXERUNIQUENESSSCORE_SEQ + increment by 50 + start with 30000000000; + +create sequence INDEXER_SEQ + increment by 50 + start with 30000000000; + +create sequence MOVIEINFO_SEQ + increment by 50 + start with 30000000000; + +create sequence NOTIFICATION_SEQ + increment by 50 + start with 30000000000; + +create sequence SEARCH_SEQ + increment by 50 + start with 30000000000; + +create sequence SHOWNNEWS_SEQ + increment by 50 + start with 30000000000; + +create sequence TVINFO_SEQ + increment by 50 + start with 30000000000; diff --git a/core/src/main/resources/migration/V4__NOTIFICATION.sql b/core/src/main/resources/migration/V4__NOTIFICATION.sql deleted file mode 100644 index 08e5097cb..000000000 --- a/core/src/main/resources/migration/V4__NOTIFICATION.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS NOTIFICATION -( - ID INTEGER PRIMARY KEY NOT NULL, - NOTIFICATION_EVENT_TYPE VARCHAR(255) NOT NULL, - MESSAGE_TYPE VARCHAR(255) NOT NULL, - TITLE VARCHAR(255), - BODY VARCHAR(255) NOT NULL, - URLS VARCHAR(255) NOT NULL, - TIME TIMESTAMP NOT NULL, - DISPLAYED BOOLEAN NOT NULL DEFAULT false -); \ No newline at end of file diff --git a/core/src/main/resources/migration/V5__NOTIFICATION_ALLOW_NULL_URLS.sql b/core/src/main/resources/migration/V5__NOTIFICATION_ALLOW_NULL_URLS.sql deleted file mode 100644 index 2ea05438c..000000000 --- a/core/src/main/resources/migration/V5__NOTIFICATION_ALLOW_NULL_URLS.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table NOTIFICATION - alter column URLS drop not null; \ No newline at end of file diff --git a/core/src/main/resources/static/js/nzbhydra.js b/core/src/main/resources/static/js/nzbhydra.js index 0b0c32b1c..bc4037033 100644 --- a/core/src/main/resources/static/js/nzbhydra.js +++ b/core/src/main/resources/static/js/nzbhydra.js @@ -1037,8 +1037,8 @@ function selectionButton() { } - -NfoModalInstanceCtrl.$inject = ["$scope", "$uibModalInstance", "nfo"];angular +NfoModalInstanceCtrl.$inject = ["$scope", "$uibModalInstance", "nfo"]; +angular .module('nzbhydraApp') .directive('searchResult', searchResult); @@ -1376,6 +1376,7 @@ function onFinishRender($timeout) { link: linkFunction } } + //Fork of https://github.com/dotansimha/angularjs-dropdown-multiselect to make it compatible with formly angular .module('nzbhydraApp') @@ -1510,6 +1511,7 @@ function dropdownMultiselectDirective() { } } + angular .module('nzbhydraApp').directive("keepFocus", ['$timeout', function ($timeout) { /* @@ -1593,6 +1595,7 @@ function indexerStateSwitch() { } } } + /* * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) * @@ -1805,9 +1808,9 @@ function hydraNews() { } - LogModalInstanceCtrl.$inject = ["$scope", "$uibModalInstance", "entry"]; -escapeHtml.$inject = ["$sanitize"];angular +escapeHtml.$inject = ["$sanitize"]; +angular .module('nzbhydraApp') .directive('hydralog', hydralog); @@ -1997,6 +2000,7 @@ function formatClassname() { } } + /* * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) * @@ -2067,6 +2071,7 @@ function hydraChecksFooter() { }); } + console.log("Checking for below Java 17."); function checkForJavaBelow17() { GenericStorageService.get("belowJava17", false).then(function (response) { @@ -2083,18 +2088,19 @@ function hydraChecksFooter() { }); } - function checkForManualUpdateTo5x() { - GenericStorageService.get("MANUAL_UPDATE_5x", false).then(function (response) { - if (response.data !== "" && response.data) { - console.log("Manual update to 5.x necessary"); + console.log("Checking for failed backup."); + + function checkForFailedBackup() { + GenericStorageService.get("FAILED_BACKUP", false).then(function (response) { + if (response.data !== "" && response.data && !response.data) { + console.log("Failed backup detected"); //headline, message, params, size, textAlign - ModalService.open("Manual update necessary", 'A new version of NZBHydra is available. Unfortunately due to some massive changes an automatic update is not possible (or advisable). Please see ' + - 'the wiki for update instructions.', { + ModalService.open("Failed backup", 'The creation of a backup file has failed. Error message: \"' + response.data.message + '."
For details please check the log around ' + response.data.time + '.', { yes: { text: "OK" } }, undefined, "left"); - GenericStorageService.put("MANUAL_UPDATE_5x", false, false); + GenericStorageService.put("FAILED_BACKUP", false, null); } }); } @@ -2137,7 +2143,7 @@ function hydraChecksFooter() { checkForOutdatedWrapper(); checkForOpenToInternet(); checkForJavaBelow17(); - checkForManualUpdateTo5x(); + checkForFailedBackup(); } function retrieveUpdateInfos() { @@ -2253,8 +2259,10 @@ function hydraChecksFooter() { welcomeIsBeingShown = false; }); } else { - _.defer(checkAndShowNews); - _.defer(checkExpiredIndexers); + if (HydraAuthService.getUserInfos().maySeeAdmin) { + _.defer(checkAndShowNews); + _.defer(checkExpiredIndexers); + } } }, function () { console.log("Error while checking for welcome") @@ -2301,7 +2309,7 @@ function hydraChecksFooter() { } } - if (ConfigService.getSafe().notificationConfig.displayNotifications) { + if (ConfigService.getSafe().notificationConfig.displayNotifications && HydraAuthService.getUserInfos().maySeeAdmin) { var socket = new SockJS(bootstrapped.baseUrl + 'websocket'); var stompClient = Stomp.over(socket); stompClient.debug = null; @@ -2483,7 +2491,7 @@ function downloaderStatusFooter() { var downloaderStatus; var updateInterval = null; - + console.log("websocket"); var socket = new SockJS(bootstrapped.baseUrl + 'websocket'); var stompClient = Stomp.over(socket); stompClient.debug = null; @@ -2792,9 +2800,9 @@ function downloadNzbsButton() { } - freetextFilter.$inject = ["DebugService"]; -booleanFilter.$inject = ["DebugService"];angular +booleanFilter.$inject = ["DebugService"]; +angular .module('nzbhydraApp').directive("columnFilterWrapper", columnFilterWrapper); function columnFilterWrapper() { @@ -3167,6 +3175,7 @@ function columnSortable() { } } + angular .module('nzbhydraApp') .directive('connectionTest', connectionTest); @@ -3255,6 +3264,7 @@ function connectionTest() { //Taken from https://github.com/IamAdamJowett/angular-click-outside clickOutside.$inject = ["$document", "$parse", "$timeout"]; + function childOf(/*child node*/c, /*parent node*/p) { //returns boolean while ((c = c.parentNode) && c !== p) ; return !!c; @@ -3398,6 +3408,7 @@ function cfgFormEntry() { }] }; } + angular .module('nzbhydraApp') .directive('hydrabackup', hydrabackup); @@ -3477,8 +3488,8 @@ function hydrabackup() { } - -addableNzbs.$inject = ["DebugService"];angular +addableNzbs.$inject = ["DebugService"]; +angular .module('nzbhydraApp') .directive('addableNzbs', addableNzbs); @@ -3506,7 +3517,8 @@ function addableNzbs(DebugService) { } -addableNzb.$inject = ["DebugService"];angular +addableNzb.$inject = ["DebugService"]; +angular .module('nzbhydraApp') .directive('addableNzb', addableNzb); @@ -3554,6 +3566,7 @@ function addableNzb(DebugService) { }; } } + /* * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) * @@ -3573,6 +3586,7 @@ function addableNzb(DebugService) { CheckCapsModalInstanceCtrl.$inject = ["$scope", "$interval", "$http", "$timeout", "growl", "capsCheckRequest"]; IndexerConfigBoxService.$inject = ["$http", "$q", "$uibModal"]; IndexerCheckBeforeCloseService.$inject = ["$q", "ModalService", "IndexerConfigBoxService", "growl", "blockUI"]; + function regexValidator(regex, message, prefixViewValue, preventEmpty) { return { expression: function ($viewValue, $modelValue) { @@ -8646,10 +8660,9 @@ function ConfigController($scope, $http, activeTab, ConfigService, config, Downl } - - UpdateService.$inject = ["$http", "growl", "blockUI", "RestartService", "RequestsErrorHandler", "$uibModal", "$timeout"]; -UpdateModalInstanceCtrl.$inject = ["$scope", "$http", "$interval", "RequestsErrorHandler"];angular +UpdateModalInstanceCtrl.$inject = ["$scope", "$http", "$interval", "RequestsErrorHandler"]; +angular .module('nzbhydraApp') .factory('UpdateService', UpdateService); @@ -10901,6 +10914,16 @@ function SearchHistoryController($scope, $state, SearchHistoryService, ConfigSer value = pair.identifierValue; } + pair = _.find(request.identifiers, function (pair) { + return pair.identifierKey === "TVMAZE" + }); + if (angular.isDefined(pair)) { + key = "TVMAZE ID"; + href = "https://www.tvmaze.com/shows/" + pair.identifierValue; + href = $filter("dereferer")(href); + value = pair.identifierValue; + } + pair = _.find(request.identifiers, function (pair) { return pair.identifierKey === "TVRAGE" }); @@ -11088,12 +11111,12 @@ function SearchController($scope, $http, $stateParams, $state, $uibModal, $timeo } if ($scope.category.searchType === "MOVIE") { - return $http.get('internalapi/autocomplete/MOVIE/', {params: {input: val}}).then(function (response) { + return $http.get('internalapi/autocomplete/MOVIE', {params: {input: val}}).then(function (response) { $scope.autocompleteLoading = false; return response.data; }); } else if ($scope.category.searchType === "TVSEARCH") { - return $http.get('internalapi/autocomplete/TV/', {params: {input: val}}).then(function (response) { + return $http.get('internalapi/autocomplete/TV', {params: {input: val}}).then(function (response) { $scope.autocompleteLoading = false; return response.data; }); @@ -12487,11 +12510,18 @@ nzbhydraapp.factory('RequestsErrorHandler', ["$q", "growl", "blockUI", "GeneralM // --- Response interceptor for handling errors generically --- responseError: function (rejection) { blockUI.reset(); + if (rejection.data instanceof ArrayBuffer) { + //The case when the response was specifically requested as that, e.g. for debug infos + rejection.data = JSON.parse(new TextDecoder().decode(rejection.data)); + } var shouldHandle = (rejection && rejection.config && rejection.status !== 403 && rejection.config.headers && rejection.config.headers[HEADER_NAME] && !rejection.config.url.contains("logerror") && !rejection.config.url.contains("/ping") && !rejection.config.alreadyHandled); if (shouldHandle) { if (rejection.data) { - var message = "An error occurred:
" + rejection.data.status + ": " + rejection.data.error; + var message = "An error occurred:
" + rejection.data.status; + if (rejection.data.error) { + message += ": " + rejection.data.error + } if (rejection.data.path) { message += "

Path: " + rejection.data.path; } @@ -12575,6 +12605,7 @@ nzbhydraapp.config(['$provide', '$httpProvider', function ($provide, $httpProvid return newHttp; }]); }]); + var filters = angular.module('filters', []); filters.filter('bytes', function () { diff --git a/core/src/main/resources/static/js/nzbhydra.js.map b/core/src/main/resources/static/js/nzbhydra.js.map index 5f6af0abb..008c94994 100644 --- a/core/src/main/resources/static/js/nzbhydra.js.map +++ b/core/src/main/resources/static/js/nzbhydra.js.map @@ -1 +1 @@ -{"version":3,"sources":["nzbhydra.js","directives/tasks.js","directives/tab-or-chart.js","directives/selection-button.js","directives/search-result.js","directives/save-or-send-torrent.js","directives/on-finish-render.js","directives/multiselect-dropdown.js","directives/keep-focus.js","directives/indexer-state-switch.js","directives/indexer-selection-button.js","directives/indexer-input.js","directives/hydra-updates.js","directives/hydra-news.js","directives/hydra-log.js","directives/hydra-checks-footer.js","directives/footer.js","directives/focus-on.js","directives/downloaderStatusFooter.js","directives/download-nzbzip-button.js","directives/download-nzbs-button.js","directives/dataTableDirectives.js","directives/connection-test.js","directives/click-outside.js","directives/cfg-form-entry.js","directives/backup.js","directives/addable-nzbs.js","directives/addable-nzb.js","config/formly-indexers.js","config/formly-downloaders.js","config/formly-config.js","config/config-service.js","config/config-fields-service.js","config/config-controller.js","update-service.js","system-controller.js","stats-service.js","stats-controller.js","search-service.js","search-results-controller.js","search-history-service.js","search-history-controller.js","search-controller.js","restart-service.js","nzbhydra-control-service.js","nzb-download-service.js","notifications-service.js","notification-history-controller.js","modal.js","modal-service.js","migration-service.js","login-controller.js","indexer-statuses-controller.js","index-controller.js","hydra-auth-service.js","header-controller.js","generic-storage-service.js","generic-error-handler.js","filters.js","file-selection-service.js","file-download-service.js","downloader-categories-service.js","download-history-controller.js","debug-service.js","categories-service.js","backup-service.js","angular-scroll.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/5BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/LA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5VA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACdA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtXA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5HA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACloCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7ZA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxlBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/wEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/NA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9VA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1/BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACheA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3HA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/GA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACvFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACfA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"nzbhydra.js","sourcesContent":["// For caching HTML templates, see http://paulsalaets.com/pre-caching-angular-templates-with-gulp\nangular.module('templates', []);\n\nvar nzbhydraapp = angular.module('nzbhydraApp', ['angular-loading-bar', 'cgBusy', 'ui.bootstrap', 'ipCookie', 'angular-growl',\n 'angular.filter', 'filters', 'ui.router', 'blockUI', 'mgcrea.ngStrap', 'angularUtils.directives.dirPagination',\n 'nvd3', 'formly', 'formlyBootstrap', 'frapontillo.bootstrap-switch', 'ui.select', 'ngSanitize', 'checklist-model',\n 'ngAria', 'ngMessages', 'ui.router.title', 'LocalStorageModule', 'angular.filter', 'ngFileUpload', 'ngCookies', 'angular.chips',\n 'templates', 'base64', 'duScroll', 'colorpicker.module']);\n\nnzbhydraapp.config(['$compileProvider', function ($compileProvider) {\n $compileProvider.debugInfoEnabled(true);\n}]);\n\nnzbhydraapp.config(['$animateProvider', function ($animateProvider) {\n}]);\n\nangular.module('nzbhydraApp').config([\"$stateProvider\", \"$urlRouterProvider\", \"$locationProvider\", \"blockUIConfig\", \"$urlMatcherFactoryProvider\", \"localStorageServiceProvider\", \"bootstrapped\", function ($stateProvider, $urlRouterProvider, $locationProvider, blockUIConfig, $urlMatcherFactoryProvider, localStorageServiceProvider, bootstrapped) {\n blockUIConfig.autoBlock = false;\n blockUIConfig.resetOnException = false;\n blockUIConfig.autoInjectBodyBlock = false;\n $urlMatcherFactoryProvider.strictMode(false);\n\n $urlRouterProvider.otherwise(\"/\");\n\n $stateProvider\n .state('root', {\n url: '',\n abstract: true,\n resolve: {\n //loginRequired: loginRequired\n },\n views: {\n 'header': {\n templateUrl: 'static/html/states/header.html',\n controller: 'HeaderController',\n resolve: {\n bootstrapped: function () {\n return bootstrapped;\n }\n }\n }\n }\n })\n .state(\"root.config\", {\n url: \"/config\",\n views: {},\n abstract: true\n })\n .state(\"root.config.main\", {\n url: \"/main\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n controllerAs: 'ctrl',\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 0;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Main)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.auth\", {\n url: \"/auth\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 1;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Auth)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.searching\", {\n url: \"/searching\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 2;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Searching)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.categories\", {\n url: \"/categories\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 3;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Categories)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.downloading\", {\n url: \"/downloading\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 4;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Downloading)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.indexers\", {\n url: \"/indexers\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 5;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.notifications\", {\n url: \"/notifications\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 6;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Notifications)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats\", {\n url: \"/stats\",\n abstract: true,\n views: {\n 'container@': {\n templateUrl: \"static/html/states/stats.html\",\n controller: [\"$scope\", \"$state\", function ($scope, $state) {\n $scope.$state = $state;\n $scope.bootstrapped = bootstrapped;\n }],\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n\n }\n }\n })\n .state(\"root.stats.main\", {\n url: \"/stats\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/main-stats.html\",\n controller: \"StatsController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.indexers\", {\n url: \"/indexers\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/indexer-statuses.html\",\n controller: IndexerStatusesController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n statuses: [\"$http\", function ($http) {\n return $http.get(\"internalapi/indexerstatuses\").then(function (response) {\n return response;\n });\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.searches\", {\n url: \"/searches\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/search-history.html\",\n controller: SearchHistoryController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n history: ['loginRequired', 'SearchHistoryService', function (loginRequired, SearchHistoryService) {\n return SearchHistoryService.getSearchHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Searches)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.downloads\", {\n url: \"/downloads\",\n views: {\n 'stats@root.stats': {\n templateUrl: 'static/html/states/download-history.html',\n controller: DownloadHistoryController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n downloads: [\"StatsService\", function (StatsService) {\n return StatsService.getDownloadHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Downloads)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.notifications\", {\n url: \"/notifications\",\n views: {\n 'stats@root.stats': {\n templateUrl: 'static/html/states/notification-history.html',\n controller: NotificationHistoryController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n preloadData: [\"StatsService\", function (StatsService) {\n return StatsService.getNotificationHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Notifications)\"\n }]\n }\n }\n }\n })\n .state(\"root.system\", {\n url: \"/system\",\n views: {},\n abstract: true\n })\n .state(\"root.system.control\", {\n url: \"/control\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 0;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System\"\n }]\n }\n }\n }\n })\n .state(\"root.system.updates\", {\n url: \"/updates\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 1;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Updates)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.log\", {\n url: \"/log\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 2;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Log)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.tasks\", {\n url: \"/tasks\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 3;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Tasks)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.backup\", {\n url: \"/backup\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 4;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Backup)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.bugreport\", {\n url: \"/bugreport\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 5;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Bug report)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.news\", {\n url: \"/news\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 6;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (News)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.about\", {\n url: \"/about\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n simpleInfos: ['$http', 'RequestsErrorHandler', function ($http, RequestsErrorHandler) {\n return RequestsErrorHandler.specificallyHandled(function () {\n return $http.get(\"internalapi/updates/simpleInfos\").then(\n function (response) {\n return response.data;\n }\n );\n });\n }],\n activeTab: [function () {\n return 7;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (About)\"\n }]\n }\n }\n }\n })\n\n .state(\"root.search\", {\n url: \"/?category&query&imdbId&tvdbId&title&season&episode&minsize&maxsize&minage&maxage&offsets&tvrageId&mode&tmdbId&indexers&tvmazeId&sortby&sortdirection\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/search.html\",\n controller: \"SearchController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Search\";\n }]\n }\n }\n }\n })\n .state(\"root.search.results\", {\n views: {\n 'results@root.search': {\n templateUrl: \"static/html/states/search-results.html\",\n controller: \"SearchResultsController\",\n controllerAs: \"srController\",\n options: {\n inherit: true\n },\n params: {\n modalInstance: null\n },\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n var title = \"Search results\";\n var details;\n if ($stateParams.title) {\n details = $stateParams.title;\n } else if ($stateParams.query) {\n details = $stateParams.query;\n }\n if (details) {\n title += \" (\" + details + \")\";\n }\n return title;\n }]\n }\n }\n }\n })\n .state(\"root.login\", {\n url: \"/login\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/login.html\",\n controller: \"LoginController\",\n resolve: {\n loginRequired: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Login\"\n }]\n }\n }\n }\n })\n ;\n\n\n $locationProvider.html5Mode(true);\n\n\n function loginRequired($q, $timeout, $state, HydraAuthService, type) {\n var deferred = $q.defer();\n var userInfos = HydraAuthService.getUserInfos();\n var allowed = false;\n if (type === \"search\") {\n allowed = !userInfos.searchRestricted || userInfos.maySeeSearch;\n } else if (type === \"stats\") {\n allowed = !userInfos.statsRestricted || userInfos.maySeeStats;\n } else if (type === \"admin\") {\n allowed = !userInfos.adminRestricted || userInfos.maySeeAdmin;\n } else {\n allowed = true;\n }\n if (allowed || userInfos.authType !== \"FORM\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n\n //Because I don't know for what state the login is required / asked I have a function for each\n\n function loginRequiredSearch($q, $timeout, $state, HydraAuthService) {\n var deferred = $q.defer();\n var userInfos = HydraAuthService.getUserInfos();\n if (!userInfos.searchRestricted || userInfos.maySeeSearch || userInfos.authType !== \"FORM\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n function loginRequiredStats($q, $timeout, $state, HydraAuthService) {\n var deferred = $q.defer();\n\n var userInfos = HydraAuthService.getUserInfos();\n if (!userInfos.statsRestricted || userInfos.maySeeStats || userInfos.authType !== \"FORM\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n function loginRequiredAdmin($q, $timeout, $state, HydraAuthService) {\n var deferred = $q.defer();\n\n var userInfos = HydraAuthService.getUserInfos();\n if (!userInfos.statsRestricted || userInfos.maySeeAdmin || userInfos.authType != \"form\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n localStorageServiceProvider\n .setPrefix('nzbhydra');\n localStorageServiceProvider\n .setNotify(true, false);\n}]);\n\n\nnzbhydraapp.config([\"paginationTemplateProvider\", function (paginationTemplateProvider) {\n paginationTemplateProvider.setPath('static/html/dirPagination.tpl.html');\n}]);\n\nnzbhydraapp.config(['cfpLoadingBarProvider', function (cfpLoadingBarProvider) {\n cfpLoadingBarProvider.latencyThreshold = 100;\n}]);\n\nnzbhydraapp.config(['growlProvider', function (growlProvider) {\n growlProvider.globalTimeToLive(5000);\n growlProvider.globalPosition('bottom-right');\n}]);\n\nnzbhydraapp.directive('ngEnter', function () {\n return function (scope, element, attr) {\n element.bind(\"keydown keypress\", function (event) {\n if (event.which === 13) {\n scope.$apply(function () {\n scope.$evalAsync(attr.ngEnter);\n });\n\n event.preventDefault();\n }\n });\n };\n});\n\nnzbhydraapp.filter('nzblink', function () {\n return function (resultItem) {\n var uri = new URI(\"internalapi/getnzb/user/\" + resultItem.searchResultId);\n return uri.toString();\n }\n});\n\nnzbhydraapp.factory('focus', [\"$rootScope\", \"$timeout\", function ($rootScope, $timeout) {\n return function (name) {\n $timeout(function () {\n $rootScope.$broadcast('focusOn', name);\n });\n }\n}]);\n\nnzbhydraapp.run([\"$rootScope\", function ($rootScope) {\n $rootScope.$on('$stateChangeSuccess',\n function (event, toState, toParams, fromState, fromParams) {\n try {\n $rootScope.title = toState.views[Object.keys(toState.views)[0]].resolve.$title[1](toParams);\n } catch (e) {\n\n }\n\n });\n}]);\n\nnzbhydraapp.filter('dereferer', [\"ConfigService\", function (ConfigService) {\n return function (url) {\n if (ConfigService.getSafe().dereferer) {\n return ConfigService.getSafe().dereferer\n .replace(\"$s\", escape(url))\n .replace(\"$us\", url);\n }\n return url;\n }\n}]);\n\nnzbhydraapp.filter('derefererExtracting', [\"ConfigService\", function (ConfigService) {\n return function (aString) {\n if (!ConfigService.getSafe().dereferer || !aString) {\n return aString\n }\n var matches = aString.match(/(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?/);\n if (matches === null) {\n return aString;\n }\n\n aString = aString\n .replace(matches[0], ConfigService.getSafe().dereferer.replace(\"$s\", escape(matches[0])))\n .replace(matches[0], ConfigService.getSafe().dereferer.replace(\"$us\", matches[0]))\n ;\n\n return aString;\n }\n}]);\n\nnzbhydraapp.filter('binsearch', [\"ConfigService\", function (ConfigService) {\n return function (url) {\n return \"http://binsearch.info/?q=\" + encodeURIComponent(url) + \"&max=100&adv_age=3000&server=\";\n }\n}]);\n\nnzbhydraapp.config([\"$provide\", function ($provide) {\n $provide.decorator(\"$exceptionHandler\", ['$delegate', '$injector', function ($delegate, $injector) {\n return function (exception, cause) {\n $delegate(exception, cause);\n try {\n\n if (angular.isDefined(exception.stack)) {\n var stack = exception.stack.split('\\n').map(function (line) {\n return line.trim();\n });\n stack = stack.join(\"\\n\");\n //$injector.get(\"$http\").put(\"internalapi/logerror\", {error: stack, cause: angular.isDefined(cause) ? cause.toString() : \"No known cause\"});\n }\n } catch (e) {\n console.error(\"Unable to log JS exception to server\", e);\n }\n };\n }]);\n}]);\n\n_.mixin({\n isNullOrEmpty: function (string) {\n return (_.isUndefined(string) || _.isNull(string) || (_.isString(string) && string.length === 0))\n }\n});\n\nnzbhydraapp.factory('sessionInjector', [\"$injector\", function ($injector) {\n var sessionInjector = {\n response: function (response) {\n if (response.headers(\"Hydra-MaySeeAdmin\") != null) {\n $injector.get(\"HydraAuthService\").setLoggedInByBasic(response.headers(\"Hydra-MaySeeStats\") == \"True\", response.headers(\"Hydra-MaySeeAdmin\") == \"True\", response.headers(\"Hydra-Username\"))\n }\n\n return response;\n }\n };\n return sessionInjector;\n}]);\n\nnzbhydraapp.config(['$httpProvider', function ($httpProvider) {\n $httpProvider.interceptors.push('sessionInjector');\n $httpProvider.defaults.xsrfCookieName = 'HYDRA-XSRF-TOKEN';\n}]);\n\nnzbhydraapp.directive('autoFocus', [\"$timeout\", function ($timeout) {\n return {\n restrict: 'AC',\n link: function (_scope, _element, attrs) {\n if (attrs.noFocus) {\n return;\n }\n $timeout(function () {\n _element[0].focus();\n }, 0);\n }\n };\n}]);\n\nnzbhydraapp.factory('responseObserver', [\"$q\", \"$window\", \"growl\", function responseObserver($q, $window, growl) {\n return {\n 'responseError': function (errorResponse) {\n switch (errorResponse.status) {\n case 403:\n growl.info(\"You are not allowed to visit that section.\");\n break;\n }\n if (angular.isDefined(errorResponse.config)) {\n errorResponse.config.alreadyHandled = true;\n }\n return $q.reject(errorResponse);\n }\n };\n}]);\n\nnzbhydraapp.config([\"$httpProvider\", function ($httpProvider) {\n $httpProvider.interceptors.push('responseObserver');\n}]);\n\n\nnzbhydraapp.factory('focus', [\"$timeout\", \"$window\", function ($timeout, $window) {\n return function (id) {\n // timeout makes sure that it is invoked after any other event has been triggered.\n // e.g. click events that need to run before the focus or\n // inputs elements that are in a disabled state but are enabled when those events\n // are triggered.\n $timeout(function () {\n var element = $window.document.getElementById(id);\n if (element)\n element.focus();\n });\n };\n}]);\n\nnzbhydraapp.directive('eventFocus', [\"focus\", function (focus) {\n return function (scope, elem, attr) {\n elem.on(attr.eventFocus, function () {\n focus(attr.eventFocusId);\n });\n\n // Removes bound events in the element itself\n // when the scope is destroyed\n scope.$on('$destroy', function () {\n elem.off(attr.eventFocus);\n });\n };\n}]);\n\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nangular\n .module('nzbhydraApp')\n .directive('hydraTasks', hydraTasks);\n\nfunction hydraTasks() {\n controller.$inject = [\"$scope\", \"$http\"];\n return {\n templateUrl: 'static/html/directives/tasks.html',\n controller: controller\n };\n\n function controller($scope, $http) {\n\n $http.get(\"internalapi/tasks\").then(function (response) {\n $scope.tasks = response.data;\n });\n\n $scope.runTask = function (taskName) {\n $http.put(\"internalapi/tasks/\" + taskName).then(function (response) {\n $scope.tasks = response.data;\n });\n }\n }\n}\n\n","angular\r\n .module('nzbhydraApp')\r\n .directive('tabOrChart', tabOrChart);\r\n\r\nfunction tabOrChart() {\r\n return {\r\n templateUrl: 'static/html/directives/tab-or-chart.html',\r\n transclude: {\r\n \"chartSlot\": \"chart\",\r\n \"tableSlot\": \"table\"\r\n },\r\n restrict: 'E',\r\n replace: true,\r\n scope: {\r\n display: \"@\"\r\n }\r\n\r\n };\r\n\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('selectionButton', selectionButton);\r\n\r\nfunction selectionButton() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/selection-button.html',\r\n scope: {\r\n selected: \"=\",\r\n selectable: \"=\",\r\n invertSelection: \"<\",\r\n selectAll: \"<\",\r\n deselectAll: \"<\",\r\n btn: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n if (angular.isUndefined($scope.btn)) {\r\n $scope.btn = \"default\"; //Will form class \"btn-default\"\r\n }\r\n\r\n if (angular.isUndefined($scope.invertSelection)) {\r\n $scope.invertSelection = function () {\r\n $scope.selected = _.difference($scope.selectable, $scope.selected);\r\n };\r\n }\r\n\r\n if (angular.isUndefined($scope.selectAll)) {\r\n $scope.selectAll = function () {\r\n $scope.selected.push.apply($scope.selected, $scope.selectable);\r\n };\r\n }\r\n\r\n if (angular.isUndefined($scope.deselectAll)) {\r\n $scope.deselectAll = function () {\r\n $scope.selected.splice(0, $scope.selected.length);\r\n };\r\n }\r\n\r\n\r\n }\r\n}\r\n\r\n","\nNfoModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"nfo\"];angular\n .module('nzbhydraApp')\n .directive('searchResult', searchResult);\n\nfunction searchResult() {\n controller.$inject = [\"$scope\", \"$element\", \"$http\", \"growl\", \"$attrs\", \"$uibModal\", \"$window\", \"DebugService\", \"localStorageService\", \"HydraAuthService\", \"ConfigService\"];\n return {\n templateUrl: 'static/html/directives/search-result.html',\n require: '^result',\n replace: false,\n scope: {\n result: \"<\",\n searchResultsControllerShared: \"<\"\n },\n controller: controller\n };\n\n\n function handleDisplay($scope, localStorageService, ConfigService) {\n //Display state / expansion\n $scope.foo.duplicatesDisplayed = localStorageService.get(\"duplicatesDisplayed\") !== null ? localStorageService.get(\"duplicatesDisplayed\") : false;\n $scope.foo.showCovers = localStorageService.get(\"showCovers\") !== null ? localStorageService.get(\"showCovers\") : true;\n $scope.foo.alwaysShowTitles = localStorageService.get(\"alwaysShowTitles\") !== null ? localStorageService.get(\"alwaysShowTitles\") : true;\n $scope.duplicatesExpanded = false;\n $scope.titlesExpanded = $scope.searchResultsControllerShared.expandGroupsByDefault;\n $scope.coverSize = ConfigService.getSafe().searching.coverSize;\n\n function calculateDisplayState() {\n $scope.resultDisplayed = ($scope.result.titleGroupIndex === 0 || $scope.titlesExpanded) && ($scope.duplicatesExpanded || $scope.result.duplicateGroupIndex === 0);\n }\n\n calculateDisplayState();\n\n $scope.toggleTitleExpansion = function () {\n $scope.titlesExpanded = !$scope.titlesExpanded;\n $scope.$emit(\"toggleTitleExpansionUp\", $scope.titlesExpanded, $scope.result.titleGroupIndicator);\n };\n\n $scope.toggleDuplicateExpansion = function () {\n $scope.duplicatesExpanded = !$scope.duplicatesExpanded;\n $scope.$emit(\"toggleDuplicateExpansionUp\", $scope.duplicatesExpanded, $scope.result.hash);\n };\n\n $scope.$on(\"toggleTitleExpansionDown\", function ($event, value, titleGroupIndicator) {\n if ($scope.result.titleGroupIndicator === titleGroupIndicator) {\n $scope.titlesExpanded = value;\n calculateDisplayState();\n }\n });\n\n $scope.$on(\"toggleDuplicateExpansionDown\", function ($event, value, hash) {\n if ($scope.result.hash === hash) {\n $scope.duplicatesExpanded = value;\n calculateDisplayState();\n }\n });\n\n $scope.$on(\"toggleShowCovers\", function ($event, value) {\n $scope.foo.showCovers = value;\n });\n\n $scope.$on(\"toggleAlwaysShowTitles\", function ($event, value) {\n $scope.foo.alwaysShowTitles = value;\n console.log(\"alwaysShowTitles: \" + alwaysShowTitles);\n });\n\n $scope.$on(\"duplicatesDisplayed\", function ($event, value) {\n $scope.foo.duplicatesDisplayed = value;\n if (!value) {\n //Collapse duplicate groups they shouldn't be displayed\n $scope.duplicatesExpanded = false;\n }\n calculateDisplayState();\n });\n\n $scope.$on(\"calculateDisplayState\", function () {\n calculateDisplayState();\n });\n }\n\n function handleSelection($scope, $element) {\n $scope.foo.selected = false;\n\n function sendSelectionEvent(isSelected) {\n $scope.$emit(\"selectionUp\", $scope.result, isSelected);\n }\n\n $scope.clickCheckbox = function (event, result) {\n var isSelected = event.currentTarget.checked;\n sendSelectionEvent(isSelected);\n $scope.$emit(\"checkboxClicked\", event, $scope.rowIndex, isSelected, event.currentTarget);\n };\n\n function isBetween(num, betweena, betweenb) {\n return (betweena <= num && num <= betweenb) || (betweena >= num && num >= betweenb);\n }\n\n $scope.$on(\"shiftClick\", function (event, startIndex, endIndex, newValue, previousClickTargetElement, newClickTargetElement) {\n var fromYlocation = $($(previousClickTargetElement).prop(\"parentNode\")).prop(\"offsetTop\");\n var newYlocation = $($(newClickTargetElement).prop(\"parentNode\")).prop(\"offsetTop\");\n var elementYlocation = $($element).prop(\"offsetTop\");\n if (!$scope.resultDisplayed) {\n return;\n }\n\n if (isBetween(elementYlocation, fromYlocation, newYlocation)) {\n sendSelectionEvent(newValue);\n $scope.foo.selected = newValue === 1;\n }\n });\n\n $scope.$on(\"invertSelection\", function () {\n if (!$scope.resultDisplayed) {\n return;\n }\n $scope.foo.selected = !$scope.foo.selected;\n sendSelectionEvent($scope.foo.selected);\n });\n\n $scope.$on(\"deselectAll\", function () {\n if (!$scope.resultDisplayed) {\n return;\n }\n $scope.foo.selected = false;\n sendSelectionEvent($scope.foo.selected);\n });\n\n $scope.$on(\"selectAll\", function () {\n if (!$scope.resultDisplayed) {\n return;\n }\n $scope.foo.selected = true;\n\n sendSelectionEvent($scope.foo.selected);\n });\n\n $scope.$on(\"toggleSelection\", function ($event, result, value) {\n if (!$scope.resultDisplayed || result !== $scope.result) {\n return;\n }\n $scope.foo.selected = value;\n });\n }\n\n function handleNfoDisplay($scope, $http, growl, $uibModal, HydraAuthService) {\n $scope.showDetailsDl = HydraAuthService.getUserInfos().maySeeDetailsDl;\n\n $scope.showNfo = showNfo;\n\n function showNfo(resultItem) {\n if (resultItem.has_nfo === 0) {\n return;\n }\n var uri = new URI(\"internalapi/nfo/\" + resultItem.searchResultId);\n return $http.get(uri.toString()).then(function (response) {\n if (response.data.successful) {\n if (response.data.hasNfo) {\n $scope.openModal(\"lg\", response.data.content)\n } else {\n growl.info(\"No NFO available\");\n }\n } else {\n growl.error(response.data.content);\n }\n });\n }\n\n $scope.openModal = openModal;\n\n function openModal(size, nfo) {\n var modalInstance = $uibModal.open({\n template: '

',\n controller: NfoModalInstanceCtrl,\n size: size,\n resolve: {\n nfo: function () {\n return nfo;\n }\n }\n });\n\n modalInstance.result.then();\n }\n\n $scope.getNfoTooltip = function () {\n if ($scope.result.hasNfo === \"YES\") {\n return \"Show NFO\"\n } else if ($scope.result.hasNfo === \"MAYBE\") {\n return \"Try to load NFO (may not be available)\";\n } else {\n return \"No NFO available\";\n }\n };\n }\n\n function handleNzbDownload($scope, $window) {\n $scope.downloadNzb = downloadNzb;\n\n function downloadNzb(resultItem) {\n //href = \"{{ result.link }}\"\n $window.location.href = resultItem.link;\n }\n }\n\n\n function controller($scope, $element, $http, growl, $attrs, $uibModal, $window, DebugService, localStorageService, HydraAuthService, ConfigService) {\n $scope.foo = {};\n handleDisplay($scope, localStorageService, ConfigService);\n handleSelection($scope, $element);\n handleNfoDisplay($scope, $http, growl, $uibModal, HydraAuthService);\n handleNzbDownload($scope, $window);\n\n $scope.kify = function () {\n return function (number) {\n if (number > 1000) {\n return Math.round(number / 1000) + \"k\";\n }\n return number;\n };\n };\n\n\n $scope.showCover = function (url) {\n console.log(\"Show \" + url);\n $uibModal.open({\n template: '
\\n' +\n ' \\n' +\n '
',\n controller: [\"$scope\", \"url\", function ($scope, url) {\n $scope.url = url;\n }],\n resolve: {\n url: function () {\n return url;\n }\n },\n size: \"md\",\n keyboard: true,\n windowTopClass: 'cover-modal-dialog'\n });\n };\n\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('NfoModalInstanceCtrl', NfoModalInstanceCtrl);\n\nfunction NfoModalInstanceCtrl($scope, $uibModalInstance, nfo) {\n\n $scope.nfo = nfo;\n\n $scope.ok = function () {\n $uibModalInstance.close($scope.selected.item);\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n}\n\nangular\n .module('nzbhydraApp')\n .filter('kify', function () {\n return function (number) {\n if (number > 1000) {\n return Math.round(number / 1000) + \"k\";\n }\n return number;\n }\n });\n","angular\r\n .module('nzbhydraApp')\r\n .directive('saveOrSendFile', saveOrSendFile);\r\n\r\nfunction saveOrSendFile() {\r\n controller.$inject = [\"$scope\", \"$http\", \"growl\", \"ConfigService\"];\r\n return {\r\n templateUrl: 'static/html/directives/save-or-send-file.html',\r\n scope: {\r\n searchResultId: \"<\",\r\n isFile: \"<\",\r\n type: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, growl, ConfigService) {\r\n $scope.cssClass = \"glyphicon-save-file\";\r\n var endpoint;\r\n if ($scope.type === \"TORRENT\") {\r\n $scope.enableButton = !_.isNullOrEmpty(ConfigService.getSafe().downloading.saveTorrentsTo) || ConfigService.getSafe().downloading.sendMagnetLinks;\r\n $scope.tooltip = \"Save torrent to black hole or send magnet link\";\r\n endpoint = \"internalapi/saveOrSendTorrent\";\r\n } else {\r\n $scope.tooltip = \"Save NZB to black hole\";\r\n $scope.enableButton = !_.isNullOrEmpty(ConfigService.getSafe().downloading.saveNzbsTo);\r\n endpoint = \"internalapi/saveNzbToBlackhole\";\r\n }\r\n $scope.add = function () {\r\n $scope.cssClass = \"nzb-spinning\";\r\n $http.put(endpoint, $scope.searchResultId).then(function (response) {\r\n if (response.data.successful) {\r\n $scope.cssClass = \"glyphicon-ok\";\r\n } else {\r\n $scope.cssClass = \"glyphicon-remove\";\r\n growl.error(response.data.message);\r\n }\r\n });\r\n };\r\n }\r\n}\r\n","//Can be used in an ng-repeat directive to call a function when the last element was rendered\n//We use it to mark the end of sorting / filtering so we can stop blocking the UI\n\nonFinishRender.$inject = [\"$timeout\"];\nangular\n .module('nzbhydraApp')\n .directive('onFinishRender', onFinishRender);\n\nfunction onFinishRender($timeout) {\n function linkFunction(scope, element, attr) {\n\n if (scope.$last === true) {\n console.log(\"Render finished\");\n // console.timeEnd(\"Presenting\");\n // console.timeEnd(\"searchall\");\n scope.$emit(\"onFinishRender\")\n }\n }\n\n return {\n link: linkFunction\n }\n}","//Fork of https://github.com/dotansimha/angularjs-dropdown-multiselect to make it compatible with formly\nangular\n .module('nzbhydraApp')\n .directive('multiselectDropdown',\n\n dropdownMultiselectDirective\n );\n\nfunction dropdownMultiselectDirective() {\n return {\n scope: {\n selectedModel: '=',\n options: '=',\n settings: '=?',\n events: '=?'\n },\n transclude: {\n toggleDropdown: '?toggleDropdown'\n },\n templateUrl: 'static/html/directives/multiselect-dropdown.html',\n controller: [\"$scope\", \"$element\", \"$filter\", \"$document\", function dropdownMultiselectController($scope, $element, $filter, $document) {\n var $dropdownTrigger = $element.children()[0];\n\n var settings = {\n showSelectedValues: true,\n showSelectAll: true,\n showDeselectAll: true,\n noSelectedText: 'None selected'\n };\n var events = {\n onToggleItem: angular.noop\n };\n angular.extend(events, $scope.events || []);\n angular.extend(settings, $scope.settings || []);\n angular.extend($scope, {settings: settings, events: events});\n\n $scope.buttonText = \"\";\n if (settings.buttonText) {\n $scope.buttonText = settings.buttonText;\n } else {\n $scope.$watch(\"selectedModel\", function () {\n if (angular.isDefined($scope.selectedModel) && settings.showSelectedValues) {\n if ($scope.selectedModel.length === 0) {\n if ($scope.settings.noSelectedText) {\n $scope.buttonText = $scope.settings.noSelectedText;\n } else {\n $scope.buttonText = \"None selected\";\n }\n } else if ($scope.selectedModel.length === $scope.options.length) {\n $scope.buttonText = \"All selected\";\n } else {\n var selected = [];\n _.each($scope.options, function (x) {\n if ($scope.selectedModel.indexOf(x.id) > -1) {\n selected.push(x.label);\n }\n })\n $scope.buttonText = selected.join(\", \");\n }\n } else {\n if (angular.isUndefined($scope.selectedModel) || ($scope.settings.noSelectedText && $scope.selectedModel.length === 0)) {\n $scope.buttonText = $scope.settings.noSelectedText;\n } else {\n $scope.buttonText = $scope.selectedModel.length + \" / \" + $scope.options.length + \" selected\";\n }\n }\n }, true);\n }\n $scope.open = false;\n\n $scope.toggleDropdown = function () {\n $scope.open = !$scope.open;\n };\n\n $scope.toggleItem = function (option) {\n var index = $scope.selectedModel.indexOf(option.id);\n var oldValue = index > -1;\n if (oldValue) {\n $scope.selectedModel.splice(index, 1);\n } else {\n $scope.selectedModel.push(option.id);\n }\n $scope.events.onToggleItem(option, !oldValue);\n };\n\n $scope.selectAll = function () {\n $scope.selectedModel = _.pluck($scope.options, \"id\");\n };\n\n $scope.deselectAll = function () {\n $scope.selectedModel.splice(0, $scope.selectedModel.length);\n };\n\n //Close when clicked outside\n\n $document.on('click', function (e) {\n function contains(collection, target) {\n var containsTarget = false;\n collection.some(function (object) {\n if (object === target) {\n containsTarget = true;\n return true;\n }\n return false;\n });\n return containsTarget;\n }\n\n if ($scope.open) {\n var target = e.target.parentElement;\n var parentFound = false;\n\n while (angular.isDefined(target) && target !== null && !parentFound) {\n if (!!target.className.split && contains(target.className.split(' '), 'multiselect-parent') && !parentFound) {\n if (target === $dropdownTrigger) {\n parentFound = true;\n }\n }\n target = target.parentElement;\n }\n\n if (!parentFound) {\n $scope.$apply(function () {\n $scope.open = false;\n });\n }\n }\n });\n\n\n }]\n\n }\n}","angular\r\n .module('nzbhydraApp').directive(\"keepFocus\", ['$timeout', function ($timeout) {\r\n /*\r\n Intended use:\r\n \r\n */\r\n return {\r\n restrict: 'A',\r\n require: 'ngModel',\r\n link: function ($scope, $element, attrs, ngModel) {\r\n\r\n ngModel.$parsers.unshift(function (value) {\r\n $timeout(function () {\r\n $element[0].focus();\r\n });\r\n return value;\r\n });\r\n\r\n }\r\n };\r\n}]);","/*\r\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .directive('indexerStateSwitch', indexerStateSwitch);\r\n\r\nfunction indexerStateSwitch() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-state-switch.html',\r\n scope: {\r\n indexer: \"=\",\r\n handleWidth: \"@\"\r\n },\r\n replace: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.value = $scope.indexer.state === \"ENABLED\";\r\n $scope.handleWidth = $scope.handleWidth || \"130px\";\r\n var initialized = false;\r\n\r\n function calculateTextAndColor() {\r\n if ($scope.indexer.state === \"DISABLED_USER\") {\r\n $scope.offText = \"Disabled by user\";\r\n $scope.offColor = \"default\";\r\n } else if ($scope.indexer.state === \"DISABLED_SYSTEM_TEMPORARY\") {\r\n $scope.offText = \"Temporary disabled\";\r\n $scope.offColor = \"warning\";\r\n } else if ($scope.indexer.state === \"DISABLED_SYSTEM\") {\r\n $scope.offText = \"Disabled by system\";\r\n $scope.offColor = \"danger\";\r\n }\r\n }\r\n\r\n calculateTextAndColor();\r\n\r\n $scope.onChange = function () {\r\n if (initialized) {\r\n //Skip on first call when initial value is set\r\n $scope.indexer.state = $scope.value ? \"ENABLED\" : \"DISABLED_USER\";\r\n calculateTextAndColor();\r\n }\r\n initialized = true;\r\n }\r\n }\r\n}","/*\r\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .directive('indexerSelectionButton', indexerSelectionButton);\r\n\r\nfunction indexerSelectionButton() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-selection-button.html',\r\n scope: {\r\n selectedIndexers: \"=\",\r\n availableIndexers: \"=\",\r\n btn: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n $scope.anyTorrentIndexersSelectable = _.any($scope.availableIndexers,\r\n function (indexer) {\r\n return indexer.searchModuleType === \"TORZNAB\";\r\n }\r\n );\r\n\r\n $scope.invertSelection = function () {\r\n _.forEach($scope.availableIndexers, function (x) {\r\n var index = _.indexOf($scope.selectedIndexers, x.name);\r\n if (index === -1) {\r\n $scope.selectedIndexers.push(x.name);\r\n } else {\r\n $scope.selectedIndexers.splice(index, 1);\r\n }\r\n });\r\n };\r\n\r\n $scope.selectAll = function () {\r\n $scope.deselectAll();\r\n $scope.selectedIndexers.push.apply($scope.selectedIndexers, _.pluck($scope.availableIndexers, \"name\"));\r\n };\r\n\r\n $scope.deselectAll = function () {\r\n $scope.selectedIndexers.splice(0, $scope.selectedIndexers.length);\r\n };\r\n\r\n function selectByPredicate(predicate) {\r\n $scope.deselectAll();\r\n $scope.selectedIndexers.push.apply($scope.selectedIndexers,\r\n _.pluck(\r\n _.filter($scope.availableIndexers,\r\n predicate\r\n ), \"name\")\r\n );\r\n }\r\n\r\n $scope.reset = function () {\r\n selectByPredicate(function (indexer) {\r\n return indexer.preselect;\r\n });\r\n };\r\n\r\n $scope.selectAllUsenet = function () {\r\n selectByPredicate(function (indexer) {\r\n return indexer.searchModuleType !== \"TORZNAB\";\r\n });\r\n };\r\n\r\n $scope.selectAllTorrent = function () {\r\n selectByPredicate(function (indexer) {\r\n return indexer.searchModuleType === \"TORZNAB\";\r\n });\r\n }\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('indexerInput', indexerInput);\r\n\r\nfunction indexerInput() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-input.html',\r\n scope: {\r\n indexer: \"=\",\r\n model: \"=\",\r\n onClick: \"=\"\r\n },\r\n replace: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.isFocused = false;\r\n\r\n $scope.onFocus = function () {\r\n $scope.isFocused = true;\r\n };\r\n\r\n $scope.onBlur = function () {\r\n $scope.isFocused = false;\r\n };\r\n\r\n var expiryWarning;\r\n if ($scope.indexer.vipExpirationDate != null && $scope.indexer.vipExpirationDate !== \"Lifetime\") {\r\n var expiryDate = moment($scope.indexer.vipExpirationDate, \"YYYY-MM-DD\");\r\n if (expiryDate < moment()) {\r\n console.log(\"Expiry date reached for indexer \" + $scope.indexer.name);\r\n expiryWarning = \"VIP access expired on \" + $scope.indexer.vipExpirationDate;\r\n } else if (expiryDate.subtract(7, 'days') < moment()) {\r\n console.log(\"Expiry date near for indexer \" + $scope.indexer.name);\r\n expiryWarning = \"VIP access will expire on \" + $scope.indexer.vipExpirationDate;\r\n }\r\n }\r\n\r\n $scope.expiryWarning = expiryWarning;\r\n if ($scope.indexer.color !== null) {\r\n $scope.style = \"background-color: \" + $scope.indexer.color.replace(\"rgb\", \"rgba\").replace(\")\", \",0.5)\")\r\n }\r\n }\r\n\r\n}\r\n\r\n","angular\n .module('nzbhydraApp')\n .directive('hydraupdates', hydraupdates);\n\nfunction hydraupdates() {\n controller.$inject = [\"$scope\", \"UpdateService\"];\n return {\n templateUrl: 'static/html/directives/updates.html',\n controller: controller\n };\n\n function controller($scope, UpdateService) {\n\n $scope.loadingPromise = UpdateService.getInfos().then(function (response) {\n $scope.currentVersion = response.data.currentVersion;\n $scope.latestVersion = response.data.latestVersion;\n $scope.latestVersionIsBeta = response.data.latestVersionIsBeta;\n $scope.betaVersion = response.data.betaVersion;\n $scope.updateAvailable = response.data.updateAvailable;\n $scope.betaUpdateAvailable = response.data.betaUpdateAvailable;\n $scope.latestVersionIgnored = response.data.latestVersionIgnored;\n $scope.changelog = response.data.changelog;\n $scope.updatedExternally = response.data.updatedExternally;\n $scope.wrapperOutdated = response.data.wrapperOutdated;\n $scope.showUpdateBannerOnUpdatedExternally = response.data.showUpdateBannerOnUpdatedExternally;\n if ($scope.updatedExternally && !$scope.showUpdateBannerOnUpdatedExternally) {\n $scope.updateAvailable = false;\n }\n });\n\n UpdateService.getVersionHistory().then(function (response) {\n $scope.versionHistory = response.data;\n });\n\n\n $scope.update = function (version) {\n UpdateService.update(version);\n };\n\n $scope.showChangelog = function (version) {\n UpdateService.showChanges(version);\n };\n\n $scope.forceUpdate = function () {\n UpdateService.update($scope.latestVersion)\n };\n }\n}\n\n","angular\r\n .module('nzbhydraApp')\r\n .directive('hydraNews', hydraNews);\r\n\r\nfunction hydraNews() {\r\n controller.$inject = [\"$scope\", \"$http\"];\r\n return {\r\n templateUrl: \"static/html/directives/news.html\",\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http) {\r\n\r\n return $http.get(\"internalapi/news\").then(function (response) {\r\n $scope.news = response.data;\r\n });\r\n\r\n\r\n }\r\n}\r\n\r\n","\r\nLogModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"entry\"];\r\nescapeHtml.$inject = [\"$sanitize\"];angular\r\n .module('nzbhydraApp')\r\n .directive('hydralog', hydralog);\r\n\r\nfunction hydralog() {\r\n controller.$inject = [\"$scope\", \"$http\", \"$interval\", \"$uibModal\", \"$sce\", \"localStorageService\", \"growl\"];\r\n return {\r\n templateUrl: \"static/html/directives/log.html\",\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, $interval, $uibModal, $sce, localStorageService, growl) {\r\n $scope.tailInterval = null;\r\n $scope.doUpdateLog = localStorageService.get(\"doUpdateLog\") !== null ? localStorageService.get(\"doUpdateLog\") : false;\r\n $scope.doTailLog = localStorageService.get(\"doTailLog\") !== null ? localStorageService.get(\"doTailLog\") : false;\r\n\r\n $scope.active = 0;\r\n $scope.currentJsonIndex = 0;\r\n $scope.hasMoreJsonLines = true;\r\n\r\n function getLog(index) {\r\n if ($scope.active === 0) {\r\n return $http.get(\"internalapi/debuginfos/jsonlogs\", {\r\n params: {\r\n offset: index,\r\n limit: 500\r\n }\r\n }).then(function (response) {\r\n var data = response.data;\r\n $scope.jsonLogLines = angular.fromJson(data.lines);\r\n $scope.hasMoreJsonLines = data.hasMore;\r\n });\r\n } else if ($scope.active === 1) {\r\n return $http.get(\"internalapi/debuginfos/currentlogfile\").then(function (response) {\r\n var data = response.data;\r\n $scope.log = $sce.trustAsHtml(data.replace(/&/g, \"&\")\r\n .replace(//g, \">\")\r\n .replace(/\"/g, \""\")\r\n .replace(/'/g, \"'\"));\r\n }, function (data) {\r\n growl.error(data)\r\n });\r\n } else if ($scope.active === 2) {\r\n return $http.get(\"internalapi/debuginfos/logfilenames\").then(function (response) {\r\n $scope.logfilenames = response.data;\r\n });\r\n }\r\n }\r\n\r\n $scope.logPromise = getLog();\r\n\r\n $scope.select = function (index) {\r\n $scope.active = index;\r\n $scope.update();\r\n };\r\n\r\n $scope.scrollToBottom = function () {\r\n document.getElementById(\"logfile\").scrollTop = 10000000;\r\n document.getElementById(\"logfile\").scrollTop = 100001000;\r\n };\r\n\r\n $scope.update = function () {\r\n getLog($scope.currentJsonIndex);\r\n if ($scope.active === 1) {\r\n $scope.scrollToBottom();\r\n }\r\n };\r\n\r\n $scope.getOlderFormatted = function () {\r\n getLog($scope.currentJsonIndex + 500).then(function () {\r\n $scope.currentJsonIndex += 500;\r\n });\r\n\r\n };\r\n\r\n $scope.getNewerFormatted = function () {\r\n var index = Math.max($scope.currentJsonIndex - 500, 0);\r\n getLog(index);\r\n $scope.currentJsonIndex = index;\r\n };\r\n\r\n function startUpdateLogInterval() {\r\n $scope.tailInterval = $interval(function () {\r\n if ($scope.active === 1) {\r\n $scope.update();\r\n if ($scope.doTailLog && $scope.active === 1) {\r\n $scope.scrollToBottom();\r\n }\r\n }\r\n }, 5000);\r\n }\r\n\r\n $scope.toggleUpdate = function (doUpdateLog) {\r\n $scope.doUpdateLog = doUpdateLog;\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n } else if ($scope.tailInterval !== null) {\r\n console.log(\"Cancelling\");\r\n $interval.cancel($scope.tailInterval);\r\n localStorageService.set(\"doTailLog\", false);\r\n $scope.doTailLog = false;\r\n }\r\n localStorageService.set(\"doUpdateLog\", $scope.doUpdateLog);\r\n };\r\n\r\n $scope.toggleTailLog = function () {\r\n localStorageService.set(\"doTailLog\", $scope.doTailLog);\r\n };\r\n\r\n $scope.openModal = function openModal(entry) {\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'log-entry.html',\r\n controller: LogModalInstanceCtrl,\r\n size: \"xl\",\r\n resolve: {\r\n entry: function () {\r\n return entry;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then();\r\n };\r\n\r\n $scope.$on('$destroy', function () {\r\n if ($scope.tailInterval !== null) {\r\n $interval.cancel($scope.tailInterval);\r\n }\r\n });\r\n\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n }\r\n\r\n\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('LogModalInstanceCtrl', LogModalInstanceCtrl);\r\n\r\nfunction LogModalInstanceCtrl($scope, $uibModalInstance, entry) {\r\n\r\n $scope.entry = entry;\r\n\r\n $scope.ok = function () {\r\n $uibModalInstance.dismiss();\r\n };\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('formatTimestamp', formatTimestamp);\r\n\r\nfunction formatTimestamp() {\r\n return function (date) {\r\n //1579392000\r\n //1579374757\r\n if (date === null || date === undefined) {\r\n return null;\r\n }\r\n if (date < 1979374757) {\r\n date *= 1000;\r\n }\r\n return moment(date).local().format(\"YYYY-MM-DD HH:mm\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('escapeHtml', escapeHtml);\r\n\r\nfunction escapeHtml($sanitize) {\r\n return function (text) {\r\n return $sanitize(text);\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('formatClassname', formatClassname);\r\n\r\nfunction formatClassname() {\r\n return function (fqn) {\r\n return fqn.substr(fqn.lastIndexOf(\".\") + 1);\r\n\r\n }\r\n}","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nNewsModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"news\"];\nWelcomeModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$state\", \"MigrationService\"];\nangular\n .module('nzbhydraApp')\n .directive('hydraChecksFooter', hydraChecksFooter);\n\nfunction hydraChecksFooter() {\n controller.$inject = [\"$scope\", \"UpdateService\", \"RequestsErrorHandler\", \"HydraAuthService\", \"$http\", \"$uibModal\", \"ConfigService\", \"GenericStorageService\", \"ModalService\", \"growl\", \"NotificationService\", \"bootstrapped\"];\n return {\n templateUrl: 'static/html/directives/checks-footer.html',\n controller: controller\n };\n\n function controller($scope, UpdateService, RequestsErrorHandler, HydraAuthService, $http, $uibModal, ConfigService, GenericStorageService, ModalService, growl, NotificationService, bootstrapped) {\n $scope.updateAvailable = false;\n $scope.checked = false;\n var welcomeIsBeingShown = false;\n\n $scope.mayUpdate = HydraAuthService.getUserInfos().maySeeAdmin;\n\n $scope.$on(\"user:loggedIn\", function () {\n if (HydraAuthService.getUserInfos().maySeeAdmin && !$scope.checked) {\n retrieveUpdateInfos();\n }\n });\n\n function checkForOutOfMemoryException() {\n GenericStorageService.get(\"outOfMemoryDetected\", false).then(function (response) {\n if (response.data !== \"\" && response.data) {\n //headline, message, params, size, textAlign\n ModalService.open(\"Out of memory error detected\", 'The log indicates that the process ran out of memory. Please increase the XMX value in the main config and restart.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"outOfMemoryDetected\", false, false);\n }\n });\n }\n\n function checkForOpenToInternet() {\n GenericStorageService.get(\"showOpenToInternetWithoutAuth\", false).then(function (response) {\n if (response.data !== \"\" && response.data) {\n //headline, message, params, size, textAlign\n ModalService.open(\"Security issue - open to internet\", 'It looks like NZBHydra is exposed to the internet without any authentication enable. Please make sure it cannot be reached from outside your network or enable an authentication method.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"showOpenToInternetWithoutAuth\", false, false);\n }\n });\n }\n\n\n function checkForJavaBelow17() {\n GenericStorageService.get(\"belowJava17\", false).then(function (response) {\n if (response.data !== \"\" && response.data) {\n console.log(\"Java below 17\");\n //headline, message, params, size, textAlign\n ModalService.open(\"Java version below 17\", 'You\\'re currently running NZBHydra2 with an older java version. A future update will require Java 17. Please install Java 17 (not higher) from here.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"belowJava17\", false, false);\n }\n });\n }\n\n function checkForManualUpdateTo5x() {\n GenericStorageService.get(\"MANUAL_UPDATE_5x\", false).then(function (response) {\n if (response.data !== \"\" && response.data) {\n console.log(\"Manual update to 5.x necessary\");\n //headline, message, params, size, textAlign\n ModalService.open(\"Manual update necessary\", 'A new version of NZBHydra is available. Unfortunately due to some massive changes an automatic update is not possible (or advisable). Please see ' +\n 'the wiki for update instructions.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"MANUAL_UPDATE_5x\", false, false);\n }\n });\n }\n\n function checkForOutdatedWrapper() {\n $http.get(\"internalapi/updates/isDisplayWrapperOutdated\").then(function (response) {\n var data = response.data;\n if (data !== undefined && data !== null && data) {\n ModalService.open(\"Outdated wrappers detected\", 'The NZBHydra wrappers (i.e. the executables or python scripts you use to run NZBHydra) seem to be outdated. Please update them.

\\n' +\n ' Shut down NZBHydra, download the latest version and extract all the relevant wrapper files into your main NZBHydra folder.
\\n' +\n ' For Windows these files are:\\n' +\n '
    \\n' +\n '
  • NZBHydra2.exe
  • \\n' +\n '
  • NZBHydra2 Console.exe
  • \\n' +\n '
\\n' +\n ' For linux these files are:\\n' +\n '
    \\n' +\n '
  • nzbhydra2
  • \\n' +\n '
  • nzbhydra2wrapper.py
  • \\n' +\n '
  • nzbhydra2wrapperPy3.py
  • \\n' +\n '
\\n' +\n ' Make sure to overwrite all of these files that already exist - you don\\'t need to update any files that aren\\'t already present.\\n' +\n '

\\n' +\n ' Afterwards start NZBHydra again.', {\n yes: {\n text: \"OK\",\n onYes: function () {\n $http.put(\"internalapi/updates/setOutdatedWrapperDetectedWarningShown\")\n }\n }\n }, undefined, \"left\");\n\n }\n });\n }\n\n if ($scope.mayUpdate) {\n retrieveUpdateInfos();\n checkForOutOfMemoryException();\n checkForOutdatedWrapper();\n checkForOpenToInternet();\n checkForJavaBelow17();\n checkForManualUpdateTo5x();\n }\n\n function retrieveUpdateInfos() {\n $scope.checked = true;\n UpdateService.getInfos().then(function (response) {\n if (response) {\n $scope.currentVersion = response.data.currentVersion;\n $scope.latestVersion = response.data.latestVersion;\n $scope.latestVersionIsBeta = response.data.latestVersionIsBeta;\n $scope.updateAvailable = response.data.updateAvailable;\n $scope.changelog = response.data.changelog;\n $scope.updatedExternally = response.data.updatedExternally;\n $scope.showUpdateBannerOnUpdatedExternally = response.data.showUpdateBannerOnUpdatedExternally;\n $scope.showWhatsNewBanner = response.data.showWhatsNewBanner;\n if ($scope.updatedExternally && !$scope.showUpdateBannerOnUpdatedExternally) {\n $scope.updateAvailable = false;\n }\n $scope.automaticUpdateToNotice = response.data.automaticUpdateToNotice;\n\n\n $scope.$emit(\"showUpdateFooter\", $scope.updateAvailable);\n $scope.$emit(\"showAutomaticUpdateFooter\", $scope.automaticUpdateToNotice);\n } else {\n $scope.$emit(\"showUpdateFooter\", false);\n }\n });\n }\n\n $scope.update = function () {\n UpdateService.update($scope.latestVersion);\n };\n\n $scope.ignore = function () {\n UpdateService.ignore($scope.latestVersion);\n $scope.updateAvailable = false;\n $scope.$emit(\"showUpdateFooter\", $scope.updateAvailable);\n };\n\n $scope.showChangelog = function () {\n UpdateService.showChanges($scope.latestVersion);\n };\n\n $scope.showChangesFromAutomaticUpdate = function () {\n UpdateService.showChangesFromAutomaticUpdate();\n $scope.automaticUpdateToNotice = null;\n $scope.$emit(\"showAutomaticUpdateFooter\", false);\n };\n\n $scope.dismissChangesFromAutomaticUpdate = function () {\n $scope.automaticUpdateToNotice = null;\n $scope.$emit(\"showAutomaticUpdateFooter\", false);\n console.log(\"Dismissing showAutomaticUpdateFooter\");\n return $http.get(\"internalapi/updates/ackAutomaticUpdateVersionHistory\").then(function (response) {\n });\n };\n\n function checkAndShowNews() {\n RequestsErrorHandler.specificallyHandled(function () {\n if (ConfigService.getSafe().showNews) {\n $http.get(\"internalapi/news/forcurrentversion\").then(function (response) {\n var data = response.data;\n if (data && data.length > 0) {\n $uibModal.open({\n templateUrl: 'static/html/news-modal.html',\n controller: NewsModalInstanceCtrl,\n size: \"lg\",\n resolve: {\n news: function () {\n return data;\n }\n }\n });\n $http.put(\"internalapi/news/saveshown\");\n }\n });\n }\n });\n }\n\n function checkExpiredIndexers() {\n _.each(ConfigService.getSafe().indexers, function (indexer) {\n if (indexer.vipExpirationDate != null && indexer.vipExpirationDate !== \"Lifetime\") {\n var expiryWarning;\n var expiryDate = moment(indexer.vipExpirationDate, \"YYYY-MM-DD\");\n var messagePrefix = \"VIP access for indexer \" + indexer.name;\n if (expiryDate < moment()) {\n expiryWarning = messagePrefix + \" expired on \" + indexer.vipExpirationDate;\n } else if (expiryDate.subtract(7, 'days') < moment()) {\n expiryWarning = messagePrefix + \" will expire on \" + indexer.vipExpirationDate;\n }\n if (expiryWarning) {\n console.log(expiryWarning);\n growl.warning(expiryWarning);\n }\n }\n });\n }\n\n function checkAndShowWelcome() {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get(\"internalapi/welcomeshown\").then(function (response) {\n if (!response.data) {\n $http.put(\"internalapi/welcomeshown\");\n var promise = $uibModal.open({\n templateUrl: 'static/html/welcome-modal.html',\n controller: WelcomeModalInstanceCtrl,\n size: \"md\"\n });\n promise.opened.then(function () {\n welcomeIsBeingShown = true;\n });\n promise.closed.then(function () {\n welcomeIsBeingShown = false;\n });\n } else {\n _.defer(checkAndShowNews);\n _.defer(checkExpiredIndexers);\n }\n }, function () {\n console.log(\"Error while checking for welcome\")\n });\n });\n }\n\n checkAndShowWelcome();\n\n function showUnreadNotifications(unreadNotifications, stompClient) {\n if (unreadNotifications.length > ConfigService.getSafe().notificationConfig.displayNotificationsMax) {\n growl.info(unreadNotifications.length + ' notifications have piled up. Go to the notification history to view them.', {disableCountDown: true});\n for (var i = 0; i < unreadNotifications.length; i++) {\n if (unreadNotifications[i].id === undefined) {\n console.log(\"Undefined ID found for notification \" + unreadNotifications[i]);\n continue;\n }\n stompClient.send(\"/app/markNotificationRead\", {}, unreadNotifications[i].id);\n }\n return;\n }\n for (var j = 0; j < unreadNotifications.length; j++) {\n var notification = unreadNotifications[j];\n var body = notification.body.replace(\"\\n\", \"
\");\n switch (notification.messageType) {\n case \"INFO\":\n growl.info(body);\n break;\n case \"SUCCESS\":\n growl.success(body);\n break;\n case \"WARNING\":\n growl.warning(body);\n break;\n case \"FAILURE\":\n growl.danger(body);\n break;\n }\n if (notification.id === undefined) {\n console.log(\"Undefined ID found for notification \" + unreadNotifications[i]);\n continue;\n }\n stompClient.send(\"/app/markNotificationRead\", {}, notification.id);\n }\n }\n\n if (ConfigService.getSafe().notificationConfig.displayNotifications) {\n var socket = new SockJS(bootstrapped.baseUrl + 'websocket');\n var stompClient = Stomp.over(socket);\n stompClient.debug = null;\n stompClient.connect({}, function (frame) {\n stompClient.subscribe('/topic/notifications', function (message) {\n showUnreadNotifications(JSON.parse(message.body), stompClient);\n });\n });\n }\n\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('NewsModalInstanceCtrl', NewsModalInstanceCtrl);\n\nfunction NewsModalInstanceCtrl($scope, $uibModalInstance, news) {\n $scope.news = news;\n $scope.close = function () {\n $uibModalInstance.dismiss();\n };\n}\n\nangular\n .module('nzbhydraApp')\n .controller('WelcomeModalInstanceCtrl', WelcomeModalInstanceCtrl);\n\nfunction WelcomeModalInstanceCtrl($scope, $uibModalInstance, $state, MigrationService) {\n $scope.close = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.startMigration = function () {\n $uibModalInstance.dismiss();\n MigrationService.migrate();\n };\n\n $scope.goToConfig = function () {\n $uibModalInstance.dismiss();\n $state.go(\"root.config.main\");\n }\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nangular\n .module('nzbhydraApp')\n .directive('footer', footer);\n\nfunction footer() {\n controller.$inject = [\"$scope\", \"$http\", \"$uibModal\", \"ConfigService\", \"GenericStorageService\", \"bootstrapped\"];\n return {\n templateUrl: 'static/html/directives/footer.html',\n controller: controller\n };\n\n function controller($scope, $http, $uibModal, ConfigService, GenericStorageService, bootstrapped) {\n $scope.updateFooterBottom = 0;\n\n var safeConfig = bootstrapped.safeConfig;\n $scope.showDownloaderStatus = safeConfig.downloading.showDownloaderStatus && _.filter(safeConfig.downloading.downloaders, function (x) {\n return x.enabled\n }).length > 0;\n $scope.showUpdateFooter = false;\n\n $scope.$on(\"showDownloaderStatus\", function (event, doShow) {\n $scope.showDownloaderStatus = doShow;\n updateFooterBottom();\n updatePaddingBottom();\n });\n $scope.$on(\"showUpdateFooter\", function (event, doShow) {\n $scope.showUpdateFooter = doShow;\n updateFooterBottom();\n updatePaddingBottom();\n });\n $scope.$on(\"showAutomaticUpdateFooter\", function (event, doShow) {\n $scope.showAutomaticUpdateFooter = doShow;\n updateFooterBottom();\n updatePaddingBottom();\n });\n\n function updateFooterBottom() {\n\n if ($scope.showDownloaderStatus) {\n if ($scope.showAutomaticUpdateFooter) {\n $scope.updateFooterBottom = 20;\n } else {\n $scope.updateFooterBottom = 38;\n }\n } else {\n $scope.updateFooterBottom = 0;\n }\n }\n\n function updatePaddingBottom() {\n var paddingBottom = 0;\n if ($scope.showDownloaderStatus) {\n paddingBottom += 30;\n }\n if ($scope.showUpdateFooter) {\n paddingBottom += 40;\n }\n $scope.paddingBottom = paddingBottom;\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-0\");\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-30\");\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-40\");\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-70\");\n var paddingBottomClass = \"padding-bottom-\" + paddingBottom;\n document.getElementById(\"wrap\").classList.add(paddingBottomClass);\n }\n\n updatePaddingBottom();\n\n updateFooterBottom();\n\n\n }\n}\n\n","angular\r\n .module('nzbhydraApp').directive('focusOn', focusOn);\r\n\r\nfunction focusOn() {\r\n return directive;\r\n\r\n function directive(scope, elem, attr) {\r\n scope.$on('focusOn', function (e, name) {\r\n if (name === attr.focusOn) {\r\n elem[0].focus();\r\n }\r\n });\r\n }\r\n}\r\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nangular\n .module('nzbhydraApp')\n .directive('downloaderStatusFooter', downloaderStatusFooter);\n\nfunction downloaderStatusFooter() {\n controller.$inject = [\"$scope\", \"$http\", \"RequestsErrorHandler\", \"HydraAuthService\", \"$interval\", \"bootstrapped\"];\n return {\n templateUrl: 'static/html/directives/downloader-status-footer.html',\n controller: controller\n };\n\n function controller($scope, $http, RequestsErrorHandler, HydraAuthService, $interval, bootstrapped) {\n\n var downloaderStatus;\n var updateInterval = null;\n\n var socket = new SockJS(bootstrapped.baseUrl + 'websocket');\n var stompClient = Stomp.over(socket);\n stompClient.debug = null;\n stompClient.connect({}, function (frame) {\n stompClient.subscribe('/topic/downloaderStatus', function (message) {\n downloaderStatus = JSON.parse(message.body);\n updateFooter(downloaderStatus);\n });\n stompClient.send(\"/app/connectDownloaderStatus\", function (message) {\n downloaderStatus = JSON.parse(message.body);\n updateFooter(downloaderStatus);\n })\n });\n\n\n $scope.$emit(\"showDownloaderStatus\", true);\n var downloadRateCounter = 0;\n\n $scope.downloaderChart = {\n options: {\n chart: {\n type: 'stackedAreaChart',\n height: 35,\n width: 300,\n margin: {\n top: 5,\n right: 0,\n bottom: 0,\n left: 0\n },\n x: function (d) {\n return d.x;\n },\n y: function (d) {\n return d.y;\n },\n interactive: true,\n useInteractiveGuideline: false,\n transitionDuration: 0,\n showControls: false,\n showLegend: false,\n showValues: false,\n duration: 0,\n tooltip: {\n valueFormatter: function (d, i) {\n return d + \" kb/s\";\n },\n keyFormatter: function () {\n return \"\";\n },\n id: \"downloader-status-tooltip\"\n },\n css: \"float:right;\"\n }\n },\n data: [{values: [], key: \"Bla\", color: '#00a950'}],\n config: {\n refreshDataOnly: true,\n deepWatchDataDepth: 0,\n deepWatchData: false,\n deepWatchOptions: false\n }\n };\n\n function updateFooter() {\n if (downloaderStatus.lastUpdateForNow && updateInterval === null) {\n //Server will send no new status updates for a while because the last two retrieved statuses are the same.\n //We must still update the footer so that the graph doesn't stand still\n console.debug(\"Retrieved last update for now, starting update interval\");\n updateInterval = $interval(function () {\n //Just put the last known rate at the end to keep it going\n $scope.downloaderChart.data[0].values.splice(0, 1);\n $scope.downloaderChart.data[0].values.push({x: downloadRateCounter++, y: downloaderStatus.lastDownloadRate});\n try {\n $scope.api.update();\n } catch (ignored) {\n }\n if (_.every($scope.downloaderChart.data[0].values, function (value) {\n return value === downloaderStatus.lastDownloadRate\n })) {\n //The bar has been filled with the latest known value, we can now stop until we get a new update\n console.debug(\"Filled the bar with last known value, stopping update interval\");\n $interval.cancel(updateInterval);\n updateInterval = null;\n }\n }, 1000);\n } else if (updateInterval !== null && !downloaderStatus.lastUpdateForNow) {\n //New data is incoming, cancel interval\n console.debug(\"Got new update, stopping update interval\")\n $interval.cancel(updateInterval);\n updateInterval = null;\n }\n\n $scope.foo = downloaderStatus;\n $scope.foo.downloaderImage = downloaderStatus.downloaderType === 'NZBGET' ? 'nzbgetlogo' : 'sabnzbdlogo';\n $scope.foo.url = downloaderStatus.url;\n //We need to splice the variable with the rates because it's watched by angular and when overwriting it we would lose the watch and it wouldn't be updated\n var maxEntriesHistory = 200;\n if ($scope.downloaderChart.data[0].values.length < maxEntriesHistory) {\n //Not yet full, just fill up\n console.debug(\"Adding data, filling bar with initial values\")\n for (var i = $scope.downloaderChart.data[0].values.length; i < maxEntriesHistory; i++) {\n if (i >= downloaderStatus.downloadingRatesInKilobytes.length) {\n break;\n }\n $scope.downloaderChart.data[0].values.push({x: downloadRateCounter++, y: downloaderStatus.downloadingRatesInKilobytes[i]});\n }\n } else {\n console.debug(\"Adding data, moving bar\")\n //Remove first one, add to the end\n $scope.downloaderChart.data[0].values.splice(0, 1);\n $scope.downloaderChart.data[0].values.push({x: downloadRateCounter++, y: downloaderStatus.lastDownloadRate});\n }\n try {\n $scope.api.update();\n } catch (ignored) {\n }\n if ($scope.foo.state === \"DOWNLOADING\") {\n $scope.foo.buttonClass = \"play\";\n } else if ($scope.foo.state === \"PAUSED\") {\n $scope.foo.buttonClass = \"pause\";\n } else if ($scope.foo.state === \"OFFLINE\") {\n $scope.foo.buttonClass = \"off\";\n } else {\n $scope.foo.buttonClass = \"time\";\n }\n $scope.foo.state = $scope.foo.state.substr(0, 1) + $scope.foo.state.substr(1).toLowerCase();\n //Bad but without the state isn't updated\n $scope.$apply();\n }\n\n }\n}\n\n","angular\r\n .module('nzbhydraApp')\r\n .directive('downloadNzbzipButton', downloadNzbzipButton);\r\n\r\nfunction downloadNzbzipButton() {\r\n controller.$inject = [\"$scope\", \"growl\", \"$http\", \"FileDownloadService\"];\r\n return {\r\n templateUrl: 'static/html/directives/download-nzbzip-button.html',\r\n require: ['^searchResults'],\r\n scope: {\r\n searchResults: \"<\",\r\n searchTitle: \"<\",\r\n callback: \"&\"\r\n },\r\n controller: controller\r\n };\r\n\r\n\r\n function controller($scope, growl, $http, FileDownloadService) {\r\n $scope.download = function () {\r\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length === 0) {\r\n growl.info(\"You should select at least one result...\");\r\n } else {\r\n var values = _.map($scope.searchResults, function (value) {\r\n return value.searchResultId;\r\n });\r\n var link = \"internalapi/nzbzip\";\r\n\r\n var searchTitle;\r\n if (angular.isDefined($scope.searchTitle)) {\r\n searchTitle = \" for \" + $scope.searchTitle.replace(\"[^a-zA-Z0-9.-]\", \"_\");\r\n } else {\r\n searchTitle = \"\";\r\n }\r\n var filename = \"NZBHydra NZBs\" + searchTitle + \".zip\";\r\n $http({method: \"post\", url: link, data: values}).then(function (response) {\r\n if (response.data.successful && response.data.zip !== null) {\r\n link = \"internalapi/nzbzipDownload\";\r\n FileDownloadService.downloadFile(link, filename, \"POST\", response.data.zipFilepath);\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: response.data.addedIds});\r\n }\r\n if (response.data.missedIds.length > 0) {\r\n growl.error(\"Unable to add \" + response.missedIds.length + \" out of \" + values.length + \" NZBs to ZIP\");\r\n }\r\n } else {\r\n growl.error(response.data.message);\r\n }\r\n }, function (data, status, headers, config) {\r\n growl.error(status);\r\n });\r\n }\r\n }\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('downloadNzbsButton', downloadNzbsButton);\r\n\r\nfunction downloadNzbsButton() {\r\n controller.$inject = [\"$scope\", \"$http\", \"NzbDownloadService\", \"ConfigService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/download-nzbs-button.html',\r\n require: ['^searchResults'],\r\n scope: {\r\n searchResults: \"<\",\r\n callback: \"&\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, NzbDownloadService, ConfigService, growl) {\r\n\r\n $scope.downloaders = NzbDownloadService.getEnabledDownloaders();\r\n $scope.blackholeEnabled = ConfigService.getSafe().downloading.saveTorrentsTo !== null;\r\n\r\n $scope.download = function (downloader) {\r\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length === 0) {\r\n growl.info(\"You should select at least one result...\");\r\n } else {\r\n\r\n var didFilterOutResults = false;\r\n var didKeepAnyResults = false;\r\n var searchResults = _.filter($scope.searchResults, function (value) {\r\n if (value.downloadType === \"NZB\") {\r\n didKeepAnyResults = true;\r\n return true;\r\n } else {\r\n console.log(\"Not sending torrent result to downloader\");\r\n didFilterOutResults = true;\r\n return false;\r\n }\r\n });\r\n if (didFilterOutResults && !didKeepAnyResults) {\r\n growl.info(\"None of the selected results were NZBs. Adding aborted\");\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: []});\r\n }\r\n return;\r\n } else if (didFilterOutResults && didKeepAnyResults) {\r\n growl.info(\"Some the selected results are torrent results which were skipped\");\r\n }\r\n\r\n var tos = _.map(searchResults, function (entry) {\r\n return {searchResultId: entry.searchResultId, originalCategory: entry.originalCategory}\r\n });\r\n\r\n NzbDownloadService.download(downloader, tos).then(function (response) {\r\n if (angular.isDefined(response.data)) {\r\n if (response !== \"dismissed\") {\r\n if (response.data.successful) {\r\n if (response.data.message == null) {\r\n growl.info(\"Successfully added all NZBs\");\r\n } else {\r\n growl.warning(response.data.message);\r\n }\r\n } else {\r\n growl.error(response.data.message);\r\n }\r\n } else {\r\n growl.error(\"Error while adding NZBs\");\r\n }\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: response.data.addedIds});\r\n }\r\n }\r\n }, function () {\r\n growl.error(\"Error while adding NZBs\");\r\n });\r\n }\r\n };\r\n\r\n $scope.sendToBlackhole = function () {\r\n var didFilterOutResults = false;\r\n var didKeepAnyResults = false;\r\n var searchResults = _.filter($scope.searchResults, function (value) {\r\n if (value.downloadType === \"TORRENT\") {\r\n didKeepAnyResults = true;\r\n return true;\r\n } else {\r\n console.log(\"Not sending NZB result to black hole\");\r\n didFilterOutResults = true;\r\n return false;\r\n }\r\n });\r\n if (didFilterOutResults && !didKeepAnyResults) {\r\n growl.info(\"None of the selected results were torrents. Adding aborted\");\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: []});\r\n }\r\n return;\r\n } else if (didFilterOutResults && didKeepAnyResults) {\r\n growl.info(\"Some the selected results are NZB results which were skipped\");\r\n }\r\n var searchResultIds = _.pluck(searchResults, \"searchResultId\");\r\n $http.put(\"internalapi/saveTorrent\", searchResultIds).then(function (response) {\r\n if (response.data.successful) {\r\n growl.info(\"Successfully saved all torrents\");\r\n } else {\r\n growl.error(response.data.message);\r\n }\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: response.data.addedIds});\r\n }\r\n });\r\n }\r\n\r\n }\r\n}\r\n\r\n","\r\nfreetextFilter.$inject = [\"DebugService\"];\r\nbooleanFilter.$inject = [\"DebugService\"];angular\r\n .module('nzbhydraApp').directive(\"columnFilterWrapper\", columnFilterWrapper);\r\n\r\nfunction columnFilterWrapper() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: 'static/html/dataTable/columnFilterOuter.html',\r\n transclude: true,\r\n controllerAs: 'columnFilterWrapperCtrl',\r\n scope: {\r\n inline: \"@\"\r\n },\r\n bindToController: true,\r\n controller: controller,\r\n link: function (scope, element, attr, ctrl) {\r\n scope.element = element;\r\n }\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n var vm = this;\r\n\r\n vm.open = false;\r\n vm.isActive = false;\r\n\r\n vm.toggle = function () {\r\n vm.open = !vm.open;\r\n if (vm.open) {\r\n $scope.$broadcast(\"opened\");\r\n }\r\n };\r\n\r\n vm.clear = function () {\r\n if (vm.open) {\r\n $scope.$broadcast(\"clear\");\r\n }\r\n };\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive, open) {\r\n vm.open = open || false;\r\n vm.isActive = isActive;\r\n });\r\n\r\n DebugService.log(\"filter-wrapper\");\r\n }\r\n\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"freetextFilter\", freetextFilter);\r\n\r\nfunction freetextFilter(DebugService) {\r\n controller.$inject = [\"$scope\", \"focus\"];\r\n return {\r\n template: '',\r\n require: \"^columnFilterWrapper\",\r\n controllerAs: 'innerController',\r\n scope: {\r\n column: \"@\",\r\n onKey: \"@\",\r\n placeholder: \"@\",\r\n tooltip: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, focus) {\r\n $scope.inline = $scope.$parent.$parent.columnFilterWrapperCtrl.inline; //Hacky way of getting the value from the outer wrapper\r\n $scope.data = {};\r\n $scope.tooltip = $scope.tooltip || \"\";\r\n\r\n $scope.$on(\"opened\", function () {\r\n focus(\"freetext-filter-input\");\r\n });\r\n\r\n function emitFilterEvent(isOpen) {\r\n isOpen = $scope.inline || isOpen;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: $scope.data.filter,\r\n filterType: \"freetext\"\r\n }, angular.isDefined($scope.data.filter) && $scope.data.filter.length > 0, isOpen);\r\n }\r\n\r\n $scope.$on(\"clear\", function () {\r\n //Don't clear but close window (event is fired when clicked outside)\r\n emitFilterEvent(false);\r\n });\r\n\r\n $scope.onKeyUp = function (keyEvent) {\r\n if (keyEvent.which === 13 || $scope.onKey) {\r\n emitFilterEvent($scope.onKey && keyEvent.which !== 13); //Keep open if triggered by key, close always when enter pressed\r\n }\r\n };\r\n DebugService.log(\"filter-freetext\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"checkboxesFilter\", checkboxesFilter);\r\n\r\nfunction checkboxesFilter() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n template: '',\r\n controllerAs: 'checkboxesFilterController',\r\n scope: {\r\n column: \"@\",\r\n entries: \"<\",\r\n preselect: \"<\",\r\n showInvert: \"<\",\r\n isBoolean: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n $scope.selected = {\r\n entries: []\r\n };\r\n $scope.active = false;\r\n\r\n if ($scope.preselect) {\r\n $scope.selected.entries.push.apply($scope.selected.entries, $scope.entries);\r\n }\r\n\r\n $scope.invert = function () {\r\n $scope.selected.entries = _.difference($scope.entries, $scope.selected.entries);\r\n };\r\n\r\n $scope.selectAll = function () {\r\n $scope.selected.entries.push.apply($scope.selected.entries, $scope.entries);\r\n };\r\n\r\n $scope.deselectAll = function () {\r\n $scope.selected.entries.splice(0, $scope.selected.entries.length);\r\n };\r\n\r\n $scope.apply = function () {\r\n $scope.active = $scope.selected.entries.length < $scope.entries.length;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: _.pluck($scope.selected.entries, \"id\"),\r\n filterType: \"checkboxes\",\r\n isBoolean: $scope.isBoolean\r\n }, $scope.active)\r\n };\r\n $scope.clear = function () {\r\n $scope.selectAll();\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: undefined,\r\n filterType: \"checkboxes\",\r\n isBoolean: $scope.isBoolean\r\n }, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n DebugService.log(\"filter-checkboxes\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"booleanFilter\", booleanFilter);\r\n\r\nfunction booleanFilter(DebugService) {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n controllerAs: 'booleanFilterController',\r\n scope: {\r\n column: \"@\",\r\n options: \"<\",\r\n preselect: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n\r\n function controller($scope) {\r\n $scope.selected = {value: $scope.options[$scope.preselect].value};\r\n $scope.active = false;\r\n\r\n $scope.apply = function () {\r\n $scope.active = $scope.selected.value !== $scope.options[0].value;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: $scope.selected.value,\r\n filterType: \"boolean\"\r\n }, $scope.active)\r\n };\r\n $scope.clear = function () {\r\n $scope.selected.value = true;\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {filterValue: undefined, filterType: \"boolean\"}, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n DebugService.log(\"filter-boolean\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"timeFilter\", timeFilter);\r\n\r\nfunction timeFilter() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n template: '',\r\n scope: {\r\n column: \"@\",\r\n selected: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n $scope.active = false;\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n $scope.apply = function () {\r\n $scope.active = $scope.selected.beforeDate || $scope.selected.afterDate;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: {\r\n after: $scope.selected.afterDate,\r\n before: $scope.selected.beforeDate\r\n }, filterType: \"time\"\r\n }, $scope.active)\r\n };\r\n $scope.clear = function () {\r\n $scope.selected.beforeDate = undefined;\r\n $scope.selected.afterDate = undefined;\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {filterValue: undefined, filterType: \"time\"}, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n DebugService.log(\"filter-time\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"numberRangeFilter\", numberRangeFilter);\r\n\r\nfunction numberRangeFilter() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n template: '',\r\n scope: {\r\n column: \"@\",\r\n min: \"<\",\r\n max: \"<\",\r\n addon: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n $scope.filterValue = {min: undefined, max: undefined};\r\n $scope.active = false;\r\n\r\n function apply() {\r\n $scope.active = $scope.filterValue.min || $scope.filterValue.max;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: $scope.filterValue,\r\n filterType: \"numberRange\"\r\n }, $scope.active)\r\n }\r\n\r\n $scope.clear = function () {\r\n $scope.filterValue = {min: undefined, max: undefined};\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: undefined,\r\n filterType: \"numberRange\",\r\n isBoolean: $scope.isBoolean\r\n }, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n\r\n $scope.apply = function () {\r\n apply();\r\n };\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n apply();\r\n }\r\n };\r\n\r\n DebugService.log(\"filter-number\");\r\n }\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"columnSortable\", columnSortable);\r\n\r\nfunction columnSortable() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: \"static/html/dataTable/columnSortable.html\",\r\n transclude: true,\r\n scope: {\r\n sortMode: \"<\", //0: no sorting, 1: asc, 2: desc\r\n column: \"@\",\r\n reversed: \"<\",\r\n startMode: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n if (angular.isUndefined($scope.sortMode)) {\r\n $scope.sortMode = 0;\r\n }\r\n\r\n if (angular.isUndefined($scope.startMode)) {\r\n $scope.startMode = 1;\r\n }\r\n\r\n $scope.sortModel = {\r\n sortMode: $scope.sortMode,\r\n column: $scope.column,\r\n reversed: $scope.reversed,\r\n startMode: $scope.startMode,\r\n active: false\r\n };\r\n\r\n $scope.$on(\"newSortColumn\", function (event, column, sortMode) {\r\n $scope.sortModel.active = column === $scope.sortModel.column;\r\n if (column !== $scope.sortModel.column) {\r\n $scope.sortModel.sortMode = 0;\r\n } else {\r\n $scope.sortModel.sortMode = sortMode;\r\n }\r\n });\r\n\r\n $scope.sort = function () {\r\n if ($scope.sortModel.sortMode === 0 || angular.isUndefined($scope.sortModel.sortMode)) {\r\n $scope.sortModel.sortMode = $scope.sortModel.startMode;\r\n } else if ($scope.sortModel.sortMode === 1) {\r\n $scope.sortModel.sortMode = 2;\r\n } else {\r\n $scope.sortModel.sortMode = 1;\r\n }\r\n $scope.$emit(\"sort\", $scope.sortModel.column, $scope.sortModel.sortMode, $scope.sortModel.reversed)\r\n };\r\n\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('connectionTest', connectionTest);\r\n\r\nfunction connectionTest() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/connection-test.html',\r\n require: ['^type', '^data'],\r\n scope: {\r\n type: \"=\",\r\n id: \"=\",\r\n data: \"=\",\r\n downloader: \"=\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.message = \"\";\r\n\r\n\r\n var testButton = \"#button-test-connection\";\r\n var testMessage = \"#message-test-connection\";\r\n\r\n function showSuccess() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-danger\");\r\n angular.element(testButton).addClass(\"btn-success\");\r\n }\r\n\r\n function showError() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-success\");\r\n angular.element(testButton).addClass(\"btn-danger\");\r\n }\r\n\r\n $scope.testConnection = function () {\r\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\r\n var myInjector = angular.injector([\"ng\"]);\r\n var $http = myInjector.get(\"$http\");\r\n var url;\r\n var params;\r\n if ($scope.type === \"downloader\") {\r\n url = \"internalapi/test_downloader\";\r\n params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};\r\n if ($scope.downloader === \"SABNZBD\") {\r\n params.apiKey = $scope.data.apiKey;\r\n params.url = $scope.data.url;\r\n } else {\r\n params.host = $scope.data.host;\r\n params.port = $scope.data.port;\r\n params.ssl = $scope.data.ssl;\r\n }\r\n } else if ($scope.data.type === \"newznab\") {\r\n url = \"internalapi/test_newznab\";\r\n params = {host: $scope.data.host, apiKey: $scope.data.apiKey};\r\n if (angular.isDefined($scope.data.username)) {\r\n params[\"username\"] = $scope.data.username;\r\n params[\"password\"] = $scope.data.password;\r\n }\r\n }\r\n $http.get(url, {params: params}).then(function (result) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click\r\n if (result.successful) {\r\n angular.element(testMessage).text(\"\");\r\n showSuccess();\r\n } else {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }\r\n\r\n }, function () {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }\r\n ).finally(function () {\r\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\r\n })\r\n }\r\n\r\n }\r\n}\r\n\r\n","//Taken from https://github.com/IamAdamJowett/angular-click-outside\r\n\r\nclickOutside.$inject = [\"$document\", \"$parse\", \"$timeout\"];\r\nfunction childOf(/*child node*/c, /*parent node*/p) { //returns boolean\r\n while ((c = c.parentNode) && c !== p) ;\r\n return !!c;\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"clickOutside\", clickOutside);\r\n\r\n/**\r\n * @ngdoc directive\r\n * @name angular-click-outside.directive:clickOutside\r\n * @description Directive to add click outside capabilities to DOM elements\r\n * @requires $document\r\n * @requires $parse\r\n * @requires $timeout\r\n **/\r\nfunction clickOutside($document, $parse, $timeout) {\r\n return {\r\n restrict: 'A',\r\n link: function ($scope, elem, attr) {\r\n\r\n // postpone linking to next digest to allow for unique id generation\r\n $timeout(function () {\r\n var classList = (attr.outsideIfNot !== undefined) ? attr.outsideIfNot.split(/[ ,]+/) : [],\r\n fn;\r\n\r\n function eventHandler(e) {\r\n var i,\r\n element,\r\n r,\r\n id,\r\n classNames,\r\n l;\r\n\r\n // check if our element already hidden and abort if so\r\n if (angular.element(elem).hasClass(\"ng-hide\")) {\r\n return;\r\n }\r\n\r\n // if there is no click target, no point going on\r\n if (!e || !e.target) {\r\n return;\r\n }\r\n\r\n if (angular.isDefined(attr.outsideIgnore) && $scope.$eval(attr.outsideIgnore)) {\r\n return;\r\n }\r\n var isChild = childOf(e.target, elem.context);\r\n if (isChild) {\r\n return;\r\n }\r\n // loop through the available elements, looking for classes in the class list that might match and so will eat\r\n for (element = e.target; element; element = element.parentNode) {\r\n // check if the element is the same element the directive is attached to and exit if so (props @CosticaPuntaru)\r\n if (element === elem[0]) {\r\n return;\r\n }\r\n\r\n // now we have done the initial checks, start gathering id's and classes\r\n id = element.id,\r\n classNames = element.className,\r\n l = classList.length;\r\n\r\n // Unwrap SVGAnimatedString classes\r\n if (classNames && classNames.baseVal !== undefined) {\r\n classNames = classNames.baseVal;\r\n }\r\n\r\n // if there are no class names on the element clicked, skip the check\r\n if (classNames || id) {\r\n\r\n // loop through the elements id's and classnames looking for exceptions\r\n for (i = 0; i < l; i++) {\r\n //prepare regex for class word matching\r\n r = new RegExp('\\\\b' + classList[i] + '\\\\b');\r\n\r\n // check for exact matches on id's or classes, but only if they exist in the first place\r\n if ((id !== undefined && id === classList[i]) || (classNames && r.test(classNames))) {\r\n // now let's exit out as it is an element that has been defined as being ignored for clicking outside\r\n return;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // if we have got this far, then we are good to go with processing the command passed in via the click-outside attribute\r\n $timeout(function () {\r\n fn = $parse(attr['clickOutside']);\r\n fn($scope, {event: e});\r\n });\r\n }\r\n\r\n // if the devices has a touchscreen, listen for this event\r\n if (_hasTouch()) {\r\n $document.on('touchstart', eventHandler);\r\n }\r\n\r\n // still listen for the click event even if there is touch to cater for touchscreen laptops\r\n $document.on('click', eventHandler);\r\n\r\n // when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around\r\n $scope.$on('$destroy', function () {\r\n if (_hasTouch()) {\r\n $document.off('touchstart', eventHandler);\r\n }\r\n\r\n $document.off('click', eventHandler);\r\n });\r\n\r\n /**\r\n * @description Private function to attempt to figure out if we are on a touch device\r\n * @private\r\n **/\r\n function _hasTouch() {\r\n // works on most browsers, IE10/11 and Surface\r\n return 'ontouchstart' in window || navigator.maxTouchPoints;\r\n }\r\n });\r\n }\r\n };\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('cfgFormEntry', cfgFormEntry);\r\n\r\nfunction cfgFormEntry() {\r\n return {\r\n templateUrl: 'static/html/directives/cfg-form-entry.html',\r\n require: [\"^title\", \"^cfg\"],\r\n scope: {\r\n title: \"@\",\r\n cfg: \"=\",\r\n help: \"@\",\r\n type: \"@?\",\r\n options: \"=?\"\r\n },\r\n controller: [\"$scope\", \"$element\", \"$attrs\", function ($scope, $element, $attrs) {\r\n $scope.type = angular.isDefined($scope.type) ? $scope.type : 'text';\r\n $scope.options = angular.isDefined($scope.type) ? $scope.$eval($attrs.options) : [];\r\n }]\r\n };\r\n}","angular\n .module('nzbhydraApp')\n .directive('hydrabackup', hydrabackup);\n\nfunction hydrabackup() {\n controller.$inject = [\"$scope\", \"BackupService\", \"Upload\", \"FileDownloadService\", \"$http\", \"RequestsErrorHandler\", \"growl\", \"RestartService\"];\n return {\n templateUrl: 'static/html/directives/backup.html',\n controller: controller\n };\n\n function controller($scope, BackupService, Upload, FileDownloadService, $http, RequestsErrorHandler, growl, RestartService) {\n $scope.refreshBackupList = function () {\n BackupService.getBackupsList().then(function (backups) {\n $scope.backups = backups;\n });\n };\n\n $scope.refreshBackupList();\n\n $scope.uploadActive = false;\n\n\n $scope.createBackupFile = function () {\n $http.get(\"internalapi/backup/backuponly\", {params: {dontdownload: true}}).then(function () {\n $scope.refreshBackupList();\n });\n };\n $scope.createAndDownloadBackupFile = function () {\n FileDownloadService.downloadFile(\"internalapi/backup/backup\", \"nzbhydra-backup-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\", \"GET\").then(function () {\n $scope.refreshBackupList();\n });\n };\n\n $scope.uploadBackupFile = function (file, errFiles) {\n RequestsErrorHandler.specificallyHandled(function () {\n\n $scope.file = file;\n $scope.errFile = errFiles && errFiles[0];\n if (file) {\n $scope.uploadActive = true;\n file.upload = Upload.upload({\n url: 'internalapi/backup/restorefile',\n file: file\n });\n\n file.upload.then(function (response) {\n if (response.data.successful) {\n $scope.uploadActive = false;\n RestartService.startCountdown(\"Upload successful. Restarting for wrapper to restore data.\");\n } else {\n file.progress = 0;\n growl.error(response.data.message)\n }\n\n }, function (response) {\n growl.error(response.data.message)\n }, function (evt) {\n file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));\n file.loaded = Math.floor(evt.loaded / 1024);\n file.total = Math.floor(evt.total / 1024);\n });\n }\n });\n };\n\n $scope.restoreFromFile = function (filename) {\n BackupService.restoreFromFile(filename).then(function () {\n RestartService.startCountdown(\"Extraction of backup successful. Restarting for wrapper to restore data.\");\n },\n function (response) {\n growl.error(response.data);\n })\n }\n\n }\n}\n\n","\naddableNzbs.$inject = [\"DebugService\"];angular\n .module('nzbhydraApp')\n .directive('addableNzbs', addableNzbs);\n\nfunction addableNzbs(DebugService) {\n controller.$inject = [\"$scope\", \"NzbDownloadService\"];\n return {\n templateUrl: 'static/html/directives/addable-nzbs.html',\n require: [],\n scope: {\n searchresult: \"<\",\n alwaysAsk: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService) {\n $scope.alwaysAsk = $scope.alwaysAsk === \"true\";\n $scope.downloaders = _.filter(NzbDownloadService.getEnabledDownloaders(), function (downloader) {\n if ($scope.searchresult.downloadType !== \"NZB\") {\n return downloader.downloadType === $scope.searchresult.downloadType\n }\n return true;\n });\n }\n}\n","\r\naddableNzb.$inject = [\"DebugService\"];angular\r\n .module('nzbhydraApp')\r\n .directive('addableNzb', addableNzb);\r\n\r\nfunction addableNzb(DebugService) {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/addable-nzb.html',\r\n scope: {\r\n searchresult: \"=\",\r\n downloader: \"<\",\r\n alwaysAsk: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService, growl) {\r\n if (!_.isNullOrEmpty($scope.downloader.iconCssClass)) {\r\n $scope.cssClass = \"fa fa-\" + $scope.downloader.iconCssClass.replace(\"fa-\", \"\").replace(\"fa \", \"\");\r\n } else {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd\" : \"nzbget\";\r\n }\r\n\r\n $scope.add = function () {\r\n var originalClass = $scope.cssClass;\r\n $scope.cssClass = \"nzb-spinning\";\r\n NzbDownloadService.download($scope.downloader, [{\r\n searchResultId: $scope.searchresult.searchResultId ? $scope.searchresult.searchResultId : $scope.searchresult.id,\r\n originalCategory: $scope.searchresult.originalCategory,\r\n mappedCategory: $scope.searchresult.category\r\n }], $scope.alwaysAsk).then(function (response) {\r\n if (response !== \"dismissed\") {\r\n if (response.data.successful && (response.data.addedIds != null && response.data.addedIds.indexOf(Number($scope.searchresult.searchResultId)) > -1)) {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd-success\" : \"nzbget-success\";\r\n } else {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(response.data.message);\r\n }\r\n } else {\r\n $scope.cssClass = originalClass;\r\n }\r\n }, function () {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(\"An unexpected error occurred while trying to contact NZBHydra or add the NZB.\");\r\n })\r\n };\r\n }\r\n}","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nCheckCapsModalInstanceCtrl.$inject = [\"$scope\", \"$interval\", \"$http\", \"$timeout\", \"growl\", \"capsCheckRequest\"];\nIndexerConfigBoxService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\nIndexerCheckBeforeCloseService.$inject = [\"$q\", \"ModalService\", \"IndexerConfigBoxService\", \"growl\", \"blockUI\"];\nfunction regexValidator(regex, message, prefixViewValue, preventEmpty) {\n return {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n if (Array.isArray(value)) {\n for (var i = 0; i < value.length; i++) {\n if (!regex.test(value[i])) {\n return false;\n }\n }\n return true;\n } else {\n return regex.test(value);\n }\n }\n return !preventEmpty;\n },\n message: (prefixViewValue ? '$viewValue + \" ' : '\" ') + message + '\"'\n };\n}\n\nfunction getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesService) {\n var fieldset = [];\n if (indexerModel.searchModuleType === \"TORZNAB\") {\n fieldset.push({\n type: 'help',\n templateOptions: {\n type: 'help',\n lines: [\"Torznab indexers can only be used for internal searches or dedicated searches using /torznab/api\"]\n }\n });\n }\n if ((indexerModel.searchModuleType === \"NEWZNAB\" || indexerModel.searchModuleType === \"TORZNAB\") && !isInitial && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n var message;\n var cssClass;\n if (!indexerModel.configComplete) {\n message = \"The config of this indexer is incomplete. Please click the button at the bottom to check its capabilities and complete its configuration.\";\n cssClass = \"alert alert-danger\";\n } else {\n message = \"The capabilities of this indexer were not checked completely. Some actually supported search types or IDs may not be usable.\";\n cssClass = \"alert alert-warning\";\n }\n fieldset.push({\n type: 'help',\n hideExpression: 'model.allCapsChecked && model.configComplete',\n templateOptions: {\n type: 'help',\n lines: [message],\n class: cssClass\n }\n });\n }\n\n var stateHelp = \"\";\n if (indexerModel.state === \"DISABLED_SYSTEM_TEMPORARY\" || indexerModel.state === \"DISABLED_SYSTEM\") {\n if (indexerModel.state === \"DISABLED_SYSTEM_TEMPORARY\") {\n stateHelp = \"The indexer was disabled by the program due to an error. It will be reenabled automatically or you can enable it manually\";\n } else {\n stateHelp = \"The indexer was disabled by the program due to error from which it cannot recover by itself. Try checking the caps to make sure it works or just enable it and see what happens.\";\n }\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push(\n {\n key: 'name',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Name',\n required: true\n },\n validators: {\n uniqueName: {\n expression: function (viewValue) {\n if (isInitial || viewValue !== indexerModel.name) {\n return _.pluck(parentModel, \"name\").indexOf(viewValue) === -1;\n }\n return true;\n },\n message: '\"Indexer \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\n },\n noComma:\n {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n return value.indexOf(\",\") === -1;\n }\n return true;\n },\n message: '\"Name may not contain a comma\"'\n }\n }\n })\n }\n\n if (indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n fieldset.push({\n key: 'state',\n type: 'horizontalIndexerStateSwitch',\n templateOptions: {\n type: 'switch',\n label: 'State',\n help: stateHelp\n }\n });\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') {\n var hostField = {\n key: 'host',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Host',\n required: true,\n placeholder: 'http://www.someindexer.com'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n };\n if (indexerModel.searchModuleType === 'TORZNAB') {\n hostField.templateOptions.help = 'If you use Jackett and have an external URL use that one';\n }\n fieldset.push(\n hostField\n );\n }\n\n if ((indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') && indexerModel.host !== 'https://feed.animetosho.org') {\n fieldset.push(\n {\n key: 'apiKey',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'API Key'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n )\n }\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push(\n {\n key: 'apiPath',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'API path',\n help: 'Path to the API. If empty /api is used',\n required: false,\n advanced: true\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') {\n fieldset.push(\n {\n key: 'username',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'Username',\n help: 'Only needed if indexer requires HTTP auth for API access (rare).'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n );\n fieldset.push(\n {\n key: 'password',\n type: 'passwordSwitch',\n hideExpression: '!model.username',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'Password',\n help: 'Only needed if indexer requires HTTP auth for API access (rare).'\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n fieldset.push(\n {\n key: 'score',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Priority',\n required: true,\n help: 'When duplicate search results are found the result from the indexer with the highest number will be selected.',\n tooltip: 'The priority determines which indexer is used if duplicate results are found (i.e. results that link to the same upload, not just results with the same name).
The result from the indexer with the highest number is shown first in the GUI and returned for API searches.'\n\n }\n });\n }\n\n fieldset.push(\n {\n key: 'timeout',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Timeout',\n min: 1,\n help: 'Supercedes the general timeout in \"Searching\".',\n advanced: true\n }\n },\n {\n key: 'schedule',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Schedule',\n help: 'Determines when an indexer should be selected. See wiki. You can enter multiple time spans. Apply values with return key.',\n advanced: true\n }\n }\n );\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push(\n {\n key: 'hitLimit',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'API hit limit',\n help: 'Maximum number of API hits since \"API hit reset time\".',\n tooltip: 'When the maximum number of API hits is reached the indexer isn\\'t used anymore. Only API hits done by NZBHydra are taken into account.'\n },\n validators: {\n greaterThanZero: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return _.isNullOrEmpty(value) || value > 0;\n },\n message: '\"Value must be greater than 0\"'\n }\n }\n },\n {\n key: 'downloadLimit',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Download limit',\n help: 'When # of downloads since \"Hit reset time\" is reached indexer will not be searched.'\n },\n validators: {\n greaterThanZero: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return _.isNullOrEmpty(value) || value > 0;\n },\n message: '\"Value must be greater than 0\"'\n }\n }\n }\n );\n fieldset.push(\n {\n key: 'hitLimitResetTime',\n type: 'horizontalInput',\n hideExpression: '!model.hitLimit && !model.downloadLimit',\n templateOptions: {\n type: 'number',\n label: 'Hit reset time',\n help: 'UTC hour of day at which the API hit counter is reset (0-23). Leave empty for a rolling reset counter.',\n tooltip: 'Either define the time of day when the counter is reset by the indexer or leave it empty to use a rolling reset counter, meaning the number of hits for the last 24h at the time of the search is limited.'\n },\n validators: {\n timeOfDay: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return value >= 0 && value <= 23;\n },\n message: '$viewValue + \" is not a valid hour of day (0-23)\"'\n }\n }\n },\n {\n key: 'loadLimitOnRandom',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Load limiting',\n help: 'If set indexer will only be picked for one out of x API searches (on average).',\n tooltip: 'For indexers with a low API hit limit you can enable load limiting. Define any number n so that the indexer will only be used for searches in 1/n cases (on average). For example if you define a load limit of 5 the indexer will only be picked every fifth search.',\n advanced: true\n },\n validators: {\n greaterThanZero: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return _.isNullOrEmpty(value) || value > 1;\n },\n message: '\"Value must be greater than 1\"'\n }\n }\n }\n );\n }\n if (indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push({\n key: 'minSeeders',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Minimum # seeders',\n help: 'Torznab results with fewer seeders will be ignored. Supercedes any setting made in the searching config.'\n }\n })\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB') {\n fieldset.push(\n {\n key: 'userAgent',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'User agent',\n help: 'Rarely needed. Will supercede the one in the main searching settings.',\n advanced: true\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB') {\n fieldset.push(\n {\n key: 'customParameters',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'Custom parameters',\n help: 'Define custom parameters to be sent to the indexer when searching. Use the format \"name=value\"Apply values with return key.',\n advanced: 'true'\n }\n }\n )\n }\n\n\n fieldset.push(\n {\n key: 'preselect',\n type: 'horizontalSwitch',\n hideExpression: 'model.enabledForSearchSource===\"EXTERNAL\"',\n templateOptions: {\n type: 'switch',\n label: 'Preselect',\n help: 'Preselect this indexer on the search page.'\n }\n }\n );\n fieldset.push(\n {\n key: 'enabledForSearchSource',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Enable for...',\n options: [\n {name: 'Internal searches only', value: 'INTERNAL'},\n {name: 'API searches only', value: 'API'},\n {name: 'All but API update queries ', value: 'ALL_BUT_RSS'},\n {name: 'Only API update queries ', value: 'ONLY_RSS'},\n {name: 'Internal and any API searches', value: 'BOTH'}\n ],\n help: 'Select for which searches this indexer will be used. \"Update queries\" are searches without query or ID (e.g. done by Sonarr periodically).',\n advanced: true\n }\n }\n );\n\n fieldset.push(\n {\n key: 'color',\n type: 'colorInput',\n templateOptions: {\n label: 'Color',\n help: 'If set it will be used in the search results to mark the indexer\\'s results.',\n tooltip: 'To mark expanded results they\\'re shown in a darker shade so it\\'s recommended to use indexer colors which not only differ in lightness',\n advanced: true\n }\n }\n );\n\n fieldset.push(\n {\n key: 'vipExpirationDate',\n type: 'horizontalInput',\n templateOptions: {\n required: false,\n label: 'VIP expiry',\n help: 'Enter when your VIP access expires and NZBHydra will track it and warn you when close to expiry. Enter as YYYY-MM-DD or \"Lifetime\".'\n },\n validators: {\n port: regexValidator(/^(\\d{4}-\\d{2}-\\d{2})|Lifetime$/, \"is no valid date (must be 'YYYY-MM-DD' or 'Lifetime')\", true, false)\n }\n }\n );\n\n if (indexerModel.searchModuleType !== \"ANIZB\" && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n var cats = CategoriesService.getWithoutAll();\n var options = _.map(cats, function (x) {\n return {id: x.name, label: x.name}\n });\n fieldset.push(\n {\n key: 'enabledCategories',\n type: 'horizontalMultiselect',\n templateOptions: {\n label: 'Categories',\n help: 'Only use indexer when searching for these and also reject results from others. Selecting none equals selecting all.',\n options: options,\n settings: {\n showSelectedValues: false,\n noSelectedText: \"None/All\"\n },\n advanced: true\n }\n }\n );\n }\n\n\n if ((indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') && !isInitial && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n fieldset.push(\n {\n key: 'supportedSearchIds',\n type: 'horizontalMultiselect',\n templateOptions: {\n label: 'Search IDs',\n options: [\n {label: 'IMDB (TV)', id: 'TVIMDB'},\n {label: 'TVDB', id: 'TVDB'},\n {label: 'TVRage', id: 'TVRAGE'},\n {label: 'Trakt', id: 'TRAKT'},\n {label: 'TVMaze', id: 'TVMAZE'},\n {label: 'IMDB', id: 'IMDB'},\n {label: 'TMDB', id: 'TMDB'}\n ],\n noSelectedText: \"None\",\n advanced: true\n }\n }\n );\n fieldset.push(\n {\n key: 'supportedSearchTypes',\n type: 'horizontalMultiselect',\n templateOptions: {\n label: 'Search types',\n options: [\n {label: 'Audio', id: 'AUDIO'},\n {label: 'Ebooks', id: 'BOOK'},\n {label: 'Movies', id: 'MOVIE'},\n {label: 'Search', id: 'SEARCH'},\n {label: 'TV', id: 'TVSEARCH'}\n ],\n buttonText: \"None\",\n advanced: true\n }\n }\n );\n fieldset.push(\n {\n type: 'horizontalCheckCaps',\n hideExpression: '!model.host || !model.name',\n templateOptions: {\n label: 'Check capabilities',\n help: 'Find out what search types and IDs the indexer supports.',\n tooltip: 'The first time an indexer is added the connection is tested. When successful the supported search IDs and types are checked. These determine if indexers allow searching for movies, shows or ebooks using meta data like the IMDB id or the author and title. Newznab indexers cannot be used until this check was completed. Click this button to execute the caps check again.'\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType === 'nzbindex') {\n fieldset.push(\n {\n key: 'generalMinSize',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Min size',\n help: 'NZBIndex returns a lot of crap with small file sizes. Set this value and all smaller results will be filtered out no matter the category'\n }\n }\n );\n }\n\n return fieldset;\n}\n\n\nfunction _showBox(indexerModel, parentModel, isInitial, $uibModal, CategoriesService, mode, form, callback) {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/config/indexer-config-box.html',\n controller: 'IndexerConfigBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n indexerModel.showAdvanced = parentModel.showAdvanced;\n return indexerModel;\n },\n fields: function () {\n return getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesService, mode);\n },\n form: function () {\n return form;\n },\n isInitial: function () {\n return isInitial\n },\n parentModel: function () {\n return parentModel;\n }\n }\n });\n\n\n modalInstance.result.then(function (returnedModel) {\n form.$setDirty(true);\n if (angular.isDefined(callback)) {\n callback(true, returnedModel);\n }\n }, function () {\n if (angular.isDefined(callback)) {\n callback(false);\n }\n });\n}\n\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n\n formlyConfigProvider.setType({\n name: 'indexers',\n templateUrl: 'static/html/config/indexer-config.html',\n controller: function ($scope, $uibModal, growl, CategoriesService) {\n $scope.showBox = showBox;\n $scope.formOptions = {formState: $scope.formState};\n $scope.showPresetSelection = showPresetSelection;\n\n function showPresetSelection() {\n $uibModal.open({\n templateUrl: 'static/html/config/indexer-config-selection.html',\n controller: 'IndexerConfigSelectionBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n return $scope.model;\n },\n form: function () {\n return $scope.form;\n }\n }\n });\n }\n\n //Called when clicking the box of an existing indexer\n function showBox(indexerModel, model) {\n _showBox(indexerModel, model, false, $uibModal, CategoriesService, \"indexer\", $scope.form)\n }\n\n }\n });\n }]);\n\n\nangular.module('nzbhydraApp').controller('IndexerConfigSelectionBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$uibModal\", \"$http\", \"model\", \"form\", \"growl\", \"CategoriesService\", \"$timeout\", function ($scope, $q, $uibModalInstance, $uibModal, $http, model, form, growl, CategoriesService, $timeout) {\n\n $scope.showBox = showBox;\n $scope.isInitial = false;\n\n $scope.select = function (modelPreset) {\n\n addEntry(modelPreset);\n $timeout(function () {\n $uibModalInstance.close();\n },\n 200);\n };\n\n $scope.readJackettConfig = function () {\n\n var indexerModel = createIndexerModel();\n indexerModel.searchModuleType = \"JACKETT_CONFIG\";\n indexerModel.isInitial = false;\n indexerModel.host = \"http://127.0.0.1:9117\";\n indexerModel.name = \"Jackett config\";\n _showBox(indexerModel, model, true, $uibModal, CategoriesService, \"jackettConfig\", form, function (isSubmitted, returnedModel) {\n if (isSubmitted) {\n //User pushed button, now we read the config\n $http.post(\"internalapi/indexer/readJackettConfig\", {existingIndexers: model, jackettConfig: returnedModel}, {\n headers: {\n \"Accept\": \"application/json;charset=utf-8\",\n \"Accept-Charset\": \"charset=utf-8\"\n }\n }).then(function (response) {\n //Replace model with new result\n model.splice(0, model.length);\n _.each(response.data.newIndexersConfig, function (x) {\n model.push(x);\n });\n growl.info(\"Added \" + response.data.addedTrackers + \" new trackers from Jackett\");\n growl.info(\"Updated \" + response.data.updatedTrackers + \" trackers from Jackett\");\n\n }, function (response) {\n ModalService.open(\"Error reading jackett config\", response.data, {}, \"md\", \"left\");\n });\n }\n });\n\n $timeout(function () {\n $uibModalInstance.close();\n },\n 200);\n };\n\n function showBox(indexerModel, model) {\n _showBox(indexerModel, model, false, $uibModal, CategoriesService, \"indexer\", form)\n }\n\n function createIndexerModel() {\n return angular.copy({\n allCapsChecked: false,\n apiKey: null,\n backend: 'NEWZNAB',\n color: null,\n configComplete: false,\n categoryMapping: null,\n downloadLimit: null,\n enabledCategories: [],\n enabledForSearchSource: \"BOTH\",\n generalMinSize: null,\n hitLimit: null,\n hitLimitResetTime: 0,\n host: null,\n loadLimitOnRandom: null,\n name: null,\n password: null,\n preselect: true,\n score: 0,\n searchModuleType: 'NEWZNAB',\n showOnSearch: true,\n state: \"ENABLED\",\n supportedSearchIds: undefined,\n supportedSearchTypes: undefined,\n timeout: null,\n username: null,\n userAgent: null\n });\n }\n\n function addEntry(preset) {\n if (checkAddingAllowed(model, preset)) {\n var indexerModel = createIndexerModel();\n if (angular.isDefined(preset)) {\n _.extend(indexerModel, preset);\n }\n\n $scope.isInitial = true;\n\n _showBox(indexerModel, model, true, $uibModal, CategoriesService, \"indexer\", form, function (isSubmitted, returnedModel) {\n if (isSubmitted) {\n //Here is where the entry is actually added to the model\n model.push(angular.isDefined(returnedModel) ? returnedModel : indexerModel);\n }\n });\n } else {\n growl.error(\"That predefined indexer is already configured.\"); //For now this is the only case where adding is forbidden so we use this hardcoded message \"for now\"... (;-))\n }\n }\n\n function checkAddingAllowed(existingIndexers, preset) {\n if (!preset || !(preset.searchModuleType === \"ANIZB\" || preset.searchModuleType === \"BINSEARCH\" || preset.searchModuleType === \"NZBINDEX\" || preset.searchModuleType === \"NZBCLUB\")) {\n return true;\n }\n return !_.any(existingIndexers, function (existingEntry) {\n return existingEntry.name === preset.name;\n });\n }\n\n $scope.newznabPresets = [\n {\n name: \"abNZB\",\n host: \"https://abnzb.com/\"\n },\n {\n name: \"altHUB\",\n host: \"https://api.althub.co.za\"\n },\n {\n name: \"Animetosho (Newznab)\",\n host: \"https://feed.animetosho.org\",\n categories: [\"Anime\"],\n supportedSearchIds: [],\n supportedSearchTypes: [\"SEARCH\"],\n allCapsChecked: true,\n configComplete: true\n },\n {\n name: \"DogNZB\",\n host: \"https://api.dognzb.cr\"\n },\n {\n name: \"Drunken Slug\",\n host: \"https://api.drunkenslug.com\"\n },\n {\n name: \"FastNZB\",\n host: \"https://fastnzb.com\"\n },\n {\n name: \"LuluNZB\",\n host: \"https://lulunzb.com\"\n },\n {\n name: \"miatrix\",\n host: \"https://www.miatrix.com\"\n },\n {\n name: \"NZB Finder\",\n host: \"https://nzbfinder.ws\"\n },\n {\n name: \"NZBCat\",\n host: \"https://nzb.cat\"\n },\n {\n name: \"nzb.su\",\n host: \"https://api.nzb.su\"\n },\n {\n name: \"NZBGeek\",\n host: \"https://api.nzbgeek.info\"\n },\n {\n name: \"NzbNdx\",\n host: \"https://www.nzbndx.com\"\n },\n {\n name: \"NzBNooB\",\n host: \"https://www.nzbnoob.com\"\n },\n {\n name: \"NzbNation\",\n host: \"http://www.nzbnation.com/\"\n },\n {\n name: \"nzbplanet\",\n host: \"https://nzbplanet.net\"\n },\n {\n name: \"omgwtfnzbs\",\n host: \"https://api.omgwtfnzbs.me\"\n },\n {\n name: \"spotweb.com\",\n host: \"https://spotweb.me\"\n },\n {\n name: \"Tabula-Rasa\",\n host: \"https://www.tabula-rasa.pw/api/v1/\"\n },\n {\n allCapsChecked: true,\n enabledForSearchSource: \"INTERNAL\",\n categories: [],\n configComplete: true,\n downloadLimit: null,\n hitLimit: null,\n hitLimitResetTime: null,\n host: \"https://binsearch.info\",\n loadLimitOnRandom: null,\n name: \"Binsearch\",\n password: null,\n preselect: true,\n score: 0,\n showOnSearch: true,\n state: \"ENABLED\",\n supportedSearchIds: [],\n supportedSearchTypes: [],\n timeout: null,\n searchModuleType: \"BINSEARCH\",\n username: null\n },\n {\n allCapsChecked: true,\n enabledForSearchSource: \"INTERNAL\",\n categories: [],\n configComplete: true,\n downloadLimit: null,\n generalMinSize: 1,\n hitLimit: null,\n hitLimitResetTime: null,\n host: \"https://nzbindex.com\",\n loadLimitOnRandom: null,\n name: \"NZBIndex\",\n password: null,\n preselect: true,\n score: 0,\n showOnSearch: true,\n state: \"ENABLED\",\n supportedSearchIds: [],\n supportedSearchTypes: [],\n timeout: null,\n searchModuleType: \"NZBINDEX\",\n username: null\n }\n ];\n $scope.newznabPresets = _.sortBy($scope.newznabPresets, function (entry) {\n return entry.name.toLowerCase()\n });\n\n $scope.torznabPresets = [\n {\n allCapsChecked: false,\n configComplete: false,\n name: \"Jackett/Cardigann\",\n host: \"http://127.0.0.1:9117/api/v2.0/indexers/YOURTRACKER/results/torznab/\",\n supportedSearchIds: undefined,\n supportedSearchTypes: undefined,\n searchModuleType: \"TORZNAB\",\n state: \"ENABLED\",\n enabledForSearchSource: \"BOTH\"\n },\n {\n categories: [\"Anime\"],\n allCapsChecked: true,\n configComplete: true,\n name: \"Animetosho (Torznab)\",\n host: \"https://feed.animetosho.org\",\n supportedSearchIds: [],\n supportedSearchTypes: [\"SEARCH\"],\n searchModuleType: \"TORZNAB\",\n state: \"ENABLED\",\n enabledForSearchSource: \"BOTH\"\n }\n ];\n\n $scope.emptyTorznabPreset = {\n allCapsChecked: false,\n configComplete: false,\n supportedSearchIds: undefined,\n supportedSearchTypes: undefined,\n searchModuleType: \"TORZNAB\",\n state: \"ENABLED\",\n enabledForSearchSource: \"BOTH\"\n };\n $scope.torznabPresets = _.sortBy($scope.torznabPresets, function (entry) {\n return entry.name.toLowerCase()\n });\n}]);\n\n\nangular.module('nzbhydraApp').controller('IndexerConfigBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$http\", \"model\", \"form\", \"fields\", \"isInitial\", \"parentModel\", \"growl\", \"IndexerCheckBeforeCloseService\", function ($scope, $q, $uibModalInstance, $http, model, form, fields, isInitial, parentModel, growl, IndexerCheckBeforeCloseService) {\n\n $scope.model = model;\n $scope.fields = fields;\n $scope.isInitial = isInitial;\n $scope.spinnerActive = false;\n $scope.needsConnectionTest = false;\n\n $scope.obSubmit = function () {\n if (model.searchModuleType === 'JACKETT_CONFIG') {\n $uibModalInstance.close(model);\n } else if (form.$valid) {\n var a = IndexerCheckBeforeCloseService.checkBeforeClose($scope, model).then(function (data) {\n if (angular.isDefined(data)) {\n $scope.model = data;\n }\n $uibModalInstance.close(data);\n });\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n angular.forEach(form.$error, function (error) {\n angular.forEach(error, function (field) {\n field.$setTouched();\n });\n });\n }\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.deleteEntry = function () {\n parentModel.splice(parentModel.indexOf(model), 1);\n $uibModalInstance.close($scope);\n };\n\n $scope.reset = function () {\n //Reset the model twice (for some reason when we do it once the search types / ids fields are empty, resetting again fixes that... (wtf))\n $scope.options.resetModel();\n $scope.options.resetModel();\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\n if (reason === \"backdrop click\") {\n $scope.reset($scope);\n }\n });\n}]);\n\n\nangular\n .module('nzbhydraApp')\n .controller('CheckCapsModalInstanceCtrl', CheckCapsModalInstanceCtrl);\n\nfunction CheckCapsModalInstanceCtrl($scope, $interval, $http, $timeout, growl, capsCheckRequest) {\n\n var updateMessagesInterval = undefined;\n\n $scope.messages = undefined;\n $http.post(\"internalapi/indexer/checkCaps\", capsCheckRequest).then(function (response) {\n $scope.$close([response.data, capsCheckRequest.indexerConfig]);\n if (response.data.length === 0) {\n growl.info(\"No indexers were checked\");\n }\n }, function () {\n $scope.$dismiss(\"Unknown error\")\n });\n\n $timeout(\n updateMessagesInterval = $interval(function () {\n $http.get(\"internalapi/indexer/checkCapsMessages\").then(function (response) {\n var map = response.data;\n var messages = [];\n for (var name in map) {\n if (map.hasOwnProperty(name)) {\n for (var i = 0; i < map[name].length; i++) {\n var message = \"\";\n if (capsCheckRequest.checkType !== \"SINGLE\") {\n message += name + \": \";\n }\n message += map[name][i];\n messages.push(message);\n }\n }\n }\n $scope.messages = messages;\n });\n\n }, 500),\n 500);\n\n\n $scope.$on('$destroy', function () {\n if (angular.isDefined(updateMessagesInterval)) {\n $interval.cancel(updateMessagesInterval);\n }\n });\n}\n\nangular\n .module('nzbhydraApp')\n .factory('IndexerConfigBoxService', IndexerConfigBoxService);\n\nfunction IndexerConfigBoxService($http, $q, $uibModal) {\n\n return {\n checkConnection: checkConnection,\n checkCaps: checkCaps\n };\n\n function checkConnection(url, settings) {\n var deferred = $q.defer();\n\n $http.post(url, settings).then(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click\n if (result.data.successful) {\n deferred.resolve({checked: true, message: null, model: result.data});\n } else {\n deferred.reject({checked: true, message: result.data.message});\n }\n }, function (result) {\n deferred.reject({checked: false, message: result.data.message});\n });\n\n return deferred.promise;\n }\n\n function checkCaps(capsCheckRequest) {\n var deferred = $q.defer();\n\n var result = $uibModal.open({\n templateUrl: 'static/html/checker-state.html',\n controller: CheckCapsModalInstanceCtrl,\n size: \"md\",\n backdrop: \"static\",\n backdropClass: \"waiting-cursor\",\n resolve: {\n capsCheckRequest: function () {\n return capsCheckRequest;\n }\n }\n });\n\n result.result.then(function (data) {\n deferred.resolve(data[0], data[1]);\n }, function (message) {\n deferred.reject(message);\n });\n\n return deferred.promise;\n }\n\n}\n\nangular\n .module('nzbhydraApp')\n .factory('IndexerCheckBeforeCloseService', IndexerCheckBeforeCloseService);\n\nfunction IndexerCheckBeforeCloseService($q, ModalService, IndexerConfigBoxService, growl, blockUI) {\n\n return {\n checkBeforeClose: checkBeforeClose\n };\n\n function checkBeforeClose(scope, model) {\n var deferred = $q.defer();\n if (model.searchModuleType === 'JACKETT_CONFIG') {\n deferred.resolve(model);\n } else if (!scope.isInitial && (!scope.needsConnectionTest || scope.form.capsChecked)) {\n checkCapsWhenClosing(scope, model).then(function () {\n deferred.resolve(model);\n }, function () {\n deferred.reject();\n });\n } else {\n scope.spinnerActive = true;\n blockUI.start(\"Testing connection...\");\n var url = \"internalapi/indexer/checkConnection\";\n IndexerConfigBoxService.checkConnection(url, model).then(function () {\n growl.info(\"Connection to the indexer tested successfully\");\n checkCapsWhenClosing(scope, model).then(function (data) {\n scope.spinnerActive = false;\n blockUI.reset();\n deferred.resolve(data);\n }, function () {\n scope.spinnerActive = false;\n blockUI.reset();\n deferred.reject();\n });\n },\n function (data) {\n scope.spinnerActive = false;\n blockUI.reset();\n handleConnectionCheckFail(ModalService, data, model, \"indexer\", deferred);\n });\n }\n return deferred.promise;\n }\n\n //Called when the indexer dialog is closed\n function checkCapsWhenClosing(scope, model) {\n var deferred = $q.defer();\n if (angular.isUndefined(model.supportedSearchIds) || angular.isUndefined(model.supportedSearchTypes)) {\n\n blockUI.start(\"New indexer found. Testing its capabilities. This may take a bit...\");\n IndexerConfigBoxService.checkCaps({indexerConfig: model, checkType: \"SINGLE\"}).then(\n function (data) {\n data = data[0]; //We get a list of results (with one result because the check type is single)\n blockUI.reset();\n scope.spinnerActive = false;\n if (data.allCapsChecked && data.configComplete) {\n growl.info(\"Successfully tested capabilites of indexer\");\n } else if (!data.allCapsChecked && data.configComplete) {\n ModalService.open(\"Incomplete caps check\", \"The capabilities of the indexer could not be checked completely. You may use it but it's recommended to repeat the check at another time.
Until then some search types or IDs may not be usable.\", {}, \"md\", \"left\");\n } else if (!data.configComplete) {\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually from the indexer config box\", {}, \"md\", \"left\");\n }\n\n deferred.resolve(data.indexerConfig);\n },\n function () {\n blockUI.reset();\n scope.spinnerActive = false;\n model.supportedSearchIds = undefined;\n model.supportedSearchTypes = undefined;\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually using the button below.\", {}, \"md\", \"left\");\n deferred.resolve();\n }).finally(\n function () {\n scope.spinnerActive = false;\n })\n } else {\n deferred.resolve();\n }\n return deferred.promise;\n }\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nDownloaderConfigBoxService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\nDownloaderCheckBeforeCloseService.$inject = [\"$q\", \"DownloaderConfigBoxService\", \"growl\", \"ModalService\", \"blockUI\"];\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n\n formlyConfigProvider.setType({\n name: 'downloaderConfig',\n templateUrl: 'static/html/config/downloader-config.html',\n controller: function ($scope, $uibModal, growl, CategoriesService, localStorageService) {\n $scope.formOptions = {formState: $scope.formState};\n $scope._showBox = _showBox;\n $scope.showBox = showBox;\n $scope.isInitial = false;\n $scope.presets = [\n {\n name: \"NZBGet\",\n downloaderType: \"NZBGET\",\n username: \"nzbgetx\",\n nzbAddingType: \"UPLOAD\",\n nzbAccessType: \"REDIRECT\",\n iconCssClass: \"\",\n downloadType: \"NZB\",\n url: \"http://nzbget:tegbzn6789@localhost:6789\"\n },\n {\n url: \"http://localhost:8080\",\n downloaderType: \"SABNZBD\",\n name: \"SABnzbd\",\n nzbAddingType: \"UPLOAD\",\n nzbAccessType: \"REDIRECT\",\n iconCssClass: \"\",\n downloadType: \"NZB\"\n }\n ];\n\n function _showBox(model, parentModel, isInitial, callback) {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/config/downloader-config-box.html',\n controller: 'DownloaderConfigBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n //Isn't properly stored in parentmodel for some reason, this works just as well\n model.showAdvanced = localStorageService.get(\"showAdvanced\");\n console.log(model.showAdvanced);\n return model;\n },\n fields: function () {\n return getDownloaderBoxFields(model, parentModel, isInitial, angular.injector(), CategoriesService);\n },\n isInitial: function () {\n return isInitial\n },\n parentModel: function () {\n return parentModel;\n },\n data: function () {\n return $scope.options.data;\n }\n }\n });\n\n\n modalInstance.result.then(function (returnedModel) {\n $scope.form.$setDirty(true);\n if (angular.isDefined(callback)) {\n callback(true, returnedModel);\n }\n }, function () {\n if (angular.isDefined(callback)) {\n callback(false);\n }\n });\n }\n\n function showBox(model, parentModel) {\n $scope._showBox(model, parentModel, false)\n }\n\n $scope.addEntry = function (entriesCollection, preset) {\n var model = angular.copy({\n enabled: true\n });\n if (angular.isDefined(preset)) {\n _.extend(model, preset);\n }\n\n $scope.isInitial = true;\n\n $scope._showBox(model, entriesCollection, true, function (isSubmitted, returnedModel) {\n if (isSubmitted) {\n //Here is where the entry is actually added to the model\n entriesCollection.push(angular.isDefined(returnedModel) ? returnedModel : model);\n }\n });\n };\n\n function getDownloaderBoxFields(model, parentModel, isInitial) {\n var fieldset = [];\n\n fieldset = _.union(fieldset, [\n {\n key: 'enabled',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Enabled'\n }\n },\n {\n key: 'name',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Name',\n required: true\n },\n validators: {\n uniqueName: {\n expression: function (viewValue) {\n if (isInitial || viewValue !== model.name) {\n return _.pluck(parentModel, \"name\").indexOf(viewValue) === -1;\n }\n return true;\n },\n message: '\"Downloader \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\n }\n }\n\n },\n {\n key: 'url',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'URL',\n help: 'URL with scheme and full path',\n required: true\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n ]);\n\n\n if (model.downloaderType === \"SABNZBD\") {\n fieldset.push({\n key: 'apiKey',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'API Key'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n })\n } else if (model.downloaderType === \"NZBGET\") {\n fieldset.push({\n key: 'username',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Username'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n });\n fieldset.push({\n key: 'password',\n type: 'passwordSwitch',\n templateOptions: {\n type: 'text',\n label: 'Password'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n })\n }\n\n fieldset = _.union(fieldset, [\n {\n key: 'defaultCategory',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Default category',\n help: 'When adding NZBs this category will be used instead of asking for the category. Write \"Use original category\", \"Use no category\" or \"Use mapped category\" to not be asked.',\n placeholder: 'Ask when downloading'\n }\n },\n {\n key: 'nzbAddingType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'NZB adding type',\n options: [\n {name: 'Send link', value: 'SEND_LINK'},\n {name: 'Upload NZB', value: 'UPLOAD'}\n ],\n help: \"How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data.\",\n tooltip: 'You can select if you want to upload the NZB to the downloader or send a Hydra link. The downloader will do the download itself. This is a matter of taste, but adding a link and redirecting the downloader is the fastest way.' +\n '
Usually the links are determined using the URL via which you call it in your browser. If your downloader cannot access NZBHydra using that URL you can set a specific URL to be used in the main downloading config.',\n advanced: true\n }\n },\n {\n key: 'addPaused',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Add paused',\n help: 'Add NZBs paused',\n advanced: true\n }\n },\n {\n key: 'iconCssClass',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Icon CSS class',\n help: 'Copy an icon name from https://fontawesome.com/v4.7.0/icons/ (e.g. \"film\")',\n placeholder: 'Default',\n tooltip: 'If you have multiple downloaders of the same type you can select an icon from the Font Awesome library. This icon will be shown in the search results and the NZB download history instead of the default downloader icon.',\n advanced: true\n }\n }\n ]);\n\n return fieldset;\n }\n }\n });\n }]);\n\n\nangular\n .module('nzbhydraApp')\n .factory('DownloaderConfigBoxService', DownloaderConfigBoxService);\n\nfunction DownloaderConfigBoxService($http, $q, $uibModal) {\n\n return {\n checkConnection: checkConnection,\n checkCaps: checkCaps\n };\n\n function checkConnection(url, settings) {\n var deferred = $q.defer();\n\n $http.post(url, settings).then(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click\n if (result.data.successful) {\n deferred.resolve({checked: true, message: null, model: result.data});\n } else {\n deferred.reject({checked: true, message: result.data.message});\n }\n }, function (result) {\n deferred.reject({checked: false, message: result.data.message});\n });\n\n return deferred.promise;\n }\n\n function checkCaps(capsCheckRequest) {\n var deferred = $q.defer();\n\n var result = $uibModal.open({\n templateUrl: 'static/html/checker-state.html',\n controller: CheckCapsModalInstanceCtrl,\n size: \"md\",\n backdrop: \"static\",\n backdropClass: \"waiting-cursor\",\n resolve: {\n capsCheckRequest: function () {\n return capsCheckRequest;\n }\n }\n });\n\n result.result.then(function (data) {\n deferred.resolve(data[0], data[1]);\n }, function (message) {\n deferred.reject(message);\n });\n\n return deferred.promise;\n }\n}\n\nangular.module('nzbhydraApp').controller('DownloaderConfigBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$http\", \"model\", \"fields\", \"isInitial\", \"parentModel\", \"data\", \"growl\", \"DownloaderCheckBeforeCloseService\", function ($scope, $q, $uibModalInstance, $http, model, fields, isInitial, parentModel, data, growl, DownloaderCheckBeforeCloseService) {\n\n $scope.model = model;\n $scope.fields = fields;\n $scope.isInitial = isInitial;\n $scope.spinnerActive = false;\n $scope.needsConnectionTest = false;\n\n $scope.obSubmit = function () {\n if ($scope.form.$valid) {\n var a = DownloaderCheckBeforeCloseService.checkBeforeClose($scope, model).then(function (data) {\n if (angular.isDefined(data)) {\n $scope.model = data;\n }\n $uibModalInstance.close(data);\n });\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n angular.forEach($scope.form.$error, function (error) {\n angular.forEach(error, function (field) {\n field.$setTouched();\n });\n });\n }\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.deleteEntry = function () {\n parentModel.splice(parentModel.indexOf(model), 1);\n $uibModalInstance.close($scope);\n };\n\n $scope.reset = function () {\n if (angular.isDefined(data.resetFunction)) {\n //Reset the model twice (for some reason when we do it once the search types / ids fields are empty, resetting again fixes that... (wtf))\n $scope.options.resetModel();\n $scope.options.resetModel();\n }\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\n if (reason === \"backdrop click\") {\n $scope.reset($scope);\n }\n });\n}]);\n\n\nangular\n .module('nzbhydraApp')\n .factory('DownloaderCheckBeforeCloseService', DownloaderCheckBeforeCloseService);\n\nfunction DownloaderCheckBeforeCloseService($q, DownloaderConfigBoxService, growl, ModalService, blockUI) {\n\n return {\n checkBeforeClose: checkBeforeClose\n };\n\n function checkBeforeClose(scope, model) {\n var deferred = $q.defer();\n if (!scope.isInitial && !scope.needsConnectionTest) {\n deferred.resolve();\n } else {\n scope.spinnerActive = true;\n blockUI.start(\"Testing connection...\");\n var url = \"internalapi/downloader/checkConnection\";\n DownloaderConfigBoxService.checkConnection(url, JSON.stringify(model)).then(function () {\n blockUI.reset();\n scope.spinnerActive = false;\n growl.info(\"Connection to the downloader tested successfully\");\n deferred.resolve();\n },\n function (data) {\n blockUI.reset();\n scope.spinnerActive = false;\n handleConnectionCheckFail(ModalService, data, model, \"downloader\", deferred);\n }).finally(function () {\n scope.spinnerActive = false;\n blockUI.reset();\n });\n }\n return deferred.promise;\n }\n}","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nhashCode = function (s) {\n return s.split(\"\").reduce(function (a, b) {\n a = ((a << 5) - a) + b.charCodeAt(0);\n return a & a\n }, 0);\n};\n\nangular\n .module('nzbhydraApp').run([\"formlyConfig\", \"formlyValidationMessages\", function (formlyConfig, formlyValidationMessages) {\n formlyValidationMessages.addStringMessage('required', 'This field is required');\n formlyValidationMessages.addStringMessage('newznabCategories', 'Invalid');\n formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'fc.$touched || form.$submitted';\n}]);\n\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n formlyConfigProvider.extras.removeChromeAutoComplete = true;\n formlyConfigProvider.extras.explicitAsync = true;\n formlyConfigProvider.disableWarnings = window.onProd;\n\n\n formlyConfigProvider.setWrapper({\n name: 'settingWrapper',\n templateUrl: 'setting-wrapper.html'\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'fieldset',\n templateUrl: 'fieldset-wrapper.html',\n controller: ['$scope', function ($scope) {\n $scope.tooltipIsOpen = false;\n }]\n });\n\n formlyConfigProvider.setType({\n name: 'help',\n template: [\n '
',\n '
',\n '
',\n '
{{ line | derefererExtracting | unsafe }}
',\n '
',\n '
',\n '
'\n ].join(' ')\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'logicalGroup',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalInput',\n extends: 'input',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalTextArea',\n extends: 'textarea',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'timeOfDay',\n extends: 'horizontalInput',\n controller: ['$scope', function ($scope) {\n $scope.model[$scope.options.key] = moment.utc($scope.model[$scope.options.key]).toDate();\n }]\n });\n\n formlyConfigProvider.setType({\n name: 'passwordSwitch',\n extends: 'horizontalInput',\n template: [\n '
',\n '',\n '',\n '',\n '
'\n ].join(' '),\n controller: function ($scope) {\n $scope.hidePassword = true;\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalChips',\n extends: 'horizontalInput',\n template: '' +\n ' ' +\n '
' +\n ' {{chip}}' +\n ' ' +\n '
' +\n '
' +\n ' ' +\n '
'\n });\n\n formlyConfigProvider.setType({\n name: 'percentInput',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'apiKeyInput',\n template: [\n '
',\n '',\n '',\n '',\n '
'\n ].join(' '),\n controller: function ($scope) {\n $scope.generate = function () {\n var result = \"\";\n var length = 24;\n var chars = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n for (var i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];\n $scope.model[$scope.options.key] = result;\n $scope.form.$setDirty(true);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'fileInput',\n extends: 'horizontalInput',\n template: [\n '
',\n '',\n '',\n '',\n '
'\n ].join(' '),\n controller: function ($scope, FileSelectionService) {\n $scope.open = function () {\n FileSelectionService.open($scope.model[$scope.options.key], $scope.to.type).then(function (selection) {\n $scope.model[$scope.options.key] = selection;\n });\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'colorInput',\n extends: 'horizontalInput',\n templateUrl: 'static/html/config/color-control.html',\n controller: function ($scope) {\n //Model format: rgb(116,18,18)\n //Input format: rgba(100,42,41,0.5)\n if (!_.isNullOrEmpty($scope.model.color)) {\n $scope.color = $scope.model.color;\n }\n $scope.convertColorToCss = function () {\n if (_.isNullOrEmpty($scope.model.color)) {\n return \"\";\n }\n return $scope.model.color.replace(\"rgb\", \"rgba\").replace(\")\", \",0.5)\");\n }\n $scope.convertColorFromInput = function () {\n if (_.isNullOrEmpty($scope.color)) {\n return;\n }\n $scope.model.color = $scope.color.replace(\"rgba\", \"rgb\").replace(\",0.5)\", \")\");\n }\n $scope.clear = function () {\n $scope.model.color = null;\n $scope.color = null;\n }\n $scope.$watch(\"model.color\", function () {\n if (!_.isNullOrEmpty($scope.model.color)) {\n $scope.color = $scope.model.color;\n }\n })\n }\n });\n\n formlyConfigProvider.setType({\n name: 'testConnection',\n templateUrl: 'button-test-connection.html'\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalTestConnection',\n extends: 'testConnection',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'customMappingTest',\n extends: 'horizontalInput',\n template: [\n '
',\n '',\n '
'\n ].join(' '),\n controller: function ($scope, $uibModal, $http) {\n var model = $scope.model;\n var modelCopy = Object.assign({}, model);\n $scope.open = function () {\n $uibModal.open({\n templateUrl: 'static/html/custom-mapping-help.html',\n controller: [\"$scope\", \"$uibModalInstance\", \"$http\", function ($scope, $uibModalInstance, $http) {\n $scope.model = modelCopy;\n $scope.cancel = function () {\n $uibModalInstance.close();\n }\n $scope.submit = function () {\n Object.assign(model, $scope.model)\n $uibModalInstance.close();\n }\n\n $scope.test = function () {\n if (!$scope.exampleInput) {\n $scope.exampleResult = \"Empty example data\";\n return;\n\n }\n $http.post('internalapi/customMapping/test', {mapping: model, exampleInput: $scope.exampleInput}).then(function (response) {\n console.log(response.data);\n console.log(response.data.output);\n if (response.data.error) {\n $scope.exampleResult = response.data.error;\n } else if (response.data.match) {\n $scope.exampleResult = response.data.output;\n } else {\n $scope.exampleResult = \"Input does not match example\";\n }\n }, function (response) {\n $scope.exampleResult = response.message;\n })\n }\n }],\n size: \"md\"\n })\n }\n }\n });\n\n function updateIndexerModel(model, indexerConfig) {\n model.supportedSearchIds = indexerConfig.supportedSearchIds;\n model.supportedSearchTypes = indexerConfig.supportedSearchTypes;\n model.categoryMapping = indexerConfig.categoryMapping;\n model.configComplete = indexerConfig.configComplete;\n model.allCapsChecked = indexerConfig.allCapsChecked;\n model.hitLimit = indexerConfig.hitLimit;\n model.downloadLimit = indexerConfig.downloadLimit;\n model.state = indexerConfig.state;\n }\n\n formlyConfigProvider.setType({\n //BUtton\n name: 'checkCaps',\n templateUrl: 'button-check-caps.html',\n controller: function ($scope, IndexerConfigBoxService, ModalService, growl) {\n $scope.message = \"\";\n $scope.uniqueId = hashCode($scope.model.name) + hashCode($scope.model.host);\n\n var testButton = \"#button-check-caps-\" + $scope.uniqueId;\n var testMessage = \"#message-check-caps-\" + $scope.uniqueId;\n\n function showSuccess() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).removeClass(\"btn-warning\");\n angular.element(testButton).addClass(\"btn-success\");\n }\n\n function showError() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-warning\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-danger\");\n }\n\n function showWarning() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-warning\");\n }\n\n\n //When button is clicked\n $scope.checkCaps = function () {\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\n IndexerConfigBoxService.checkCaps({\n indexerConfig: $scope.model,\n checkType: \"SINGLE\"\n }).then(function (data) {\n data = data[0]; //We get a list of results (with one result because the check type is single)\n //Formly doesn't allow replacing the model so we need to set all the relevant values ourselves\n updateIndexerModel($scope.model, data.indexerConfig);\n if (data.indexerConfig.supportedSearchIds.length > 0) {\n var message = \"Supports \" + data.indexerConfig.supportedSearchIds;\n angular.element(testMessage).text(message);\n }\n if (data.indexerConfig.allCapsChecked && data.indexerConfig.configComplete) {\n showSuccess();\n growl.info(\"Successfully tested capabilites of indexer\");\n $scope.form.capsChecked = true;\n } else if (!data.indexerConfig.allCapsChecked && data.indexerConfig.configComplete) {\n showWarning();\n ModalService.open(\"Incomplete caps check\", \"The capabilities of the indexer could not be checked completely. You may use it but it's recommended to repeat the check at another time.
Until then some search types or IDs may not be usable.\", {}, \"md\", \"left\");\n $scope.form.capsChecked = true;\n } else if (!data.configComplete) {\n showError();\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually from the indexer config box\", {}, \"md\", \"left\");\n }\n }, function (message) {\n angular.element(testMessage).text(message);\n showError();\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually from the indexer config box\", {}, \"md\", \"left\");\n }).finally(function () {\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\n });\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalCheckCaps',\n extends: 'checkCaps',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalApiKeyInput',\n extends: 'apiKeyInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalPercentInput',\n extends: 'percentInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'switch',\n template: '
'\n });\n\n formlyConfigProvider.setType({\n name: 'indexerStateSwitch',\n template: ''\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalIndexerStateSwitch',\n extends: 'indexerStateSwitch',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'duoSetting',\n extends: 'input',\n defaultOptions: {\n className: 'col-md-9',\n templateOptions: {\n type: 'number',\n noRow: true,\n label: ''\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSwitch',\n extends: 'switch',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSelect',\n extends: 'select',\n wrapper: ['settingWrapper', 'bootstrapHasError'],\n controller: function ($scope) {\n if ($scope.options.templateOptions.optionsFunction !== undefined) {\n $scope.to.options.push.apply($scope.to.options, $scope.options.templateOptions.optionsFunction($scope.model));\n }\n if ($scope.options.templateOptions.optionsFunctionAfter !== undefined) {\n $scope.options.templateOptions.optionsFunctionAfter($scope.model);\n }\n }\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalMultiselect',\n defaultOptions: {\n templateOptions: {\n optionsAttr: 'bs-options',\n ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search'\n }\n },\n template: '',\n controller: function ($scope) {\n var settings = $scope.to.settings || [];\n settings.classes = settings.classes || [];\n angular.extend(settings.classes, [\"form-control\"]);\n $scope.settings = settings;\n if ($scope.options.templateOptions.optionsFunction !== null && $scope.options.templateOptions.optionsFunction !== undefined) {\n $scope.to.options.push.apply($scope.to.options, $scope.options.templateOptions.optionsFunction($scope.model));\n }\n $scope.events = {\n onToggleItem: function (item, newValue) {\n $scope.form.$setDirty(true);\n }\n }\n },\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'label',\n template: ''\n });\n\n formlyConfigProvider.setType({\n name: 'duolabel',\n extends: 'label',\n defaultOptions: {\n className: 'col-md-2',\n templateOptions: {\n label: '-'\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'repeatSection',\n templateUrl: 'repeatSection.html',\n controller: function ($scope) {\n $scope.formOptions = {formState: $scope.formState};\n $scope.addNew = addNew;\n $scope.remove = remove;\n $scope.copyFields = copyFields;\n\n function copyFields(fields) {\n fields = angular.copy(fields);\n $scope.repeatfields = fields;\n return fields;\n }\n\n $scope.clear = function (field) {\n return _.mapObject(field, function (key, val) {\n if (typeof val === 'object') {\n return $scope.clear(val);\n }\n return undefined;\n\n });\n };\n\n function addNew(preset) {\n console.log(preset);\n $scope.form.$setDirty(true);\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\n var repeatsection = $scope.model[$scope.options.key];\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\n Object.assign(newsection, preset);\n repeatsection.push(newsection);\n }\n\n function remove($index) {\n $scope.model[$scope.options.key].splice($index, 1);\n $scope.form.$setDirty(true);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'recheckAllCaps',\n templateUrl: 'static/html/config/recheck-all-caps.html',\n controller: function ($scope, $uibModal, growl, IndexerConfigBoxService) {\n $scope.recheck = function (checkType) {\n IndexerConfigBoxService.checkCaps({checkType: checkType}).then(function (listOfResults) {\n //A bit ugly, but we have to update the current model with the new data from the list\n for (var i = 0; i < $scope.model.length; i++) {\n for (var j = 0; j < listOfResults.length; j++) {\n if ($scope.model[i].name === listOfResults[j].indexerConfig.name) {\n updateIndexerModel($scope.model[i], listOfResults[j].indexerConfig);\n $scope.form.$setDirty(true);\n }\n }\n }\n });\n }\n }\n });\n\n\n formlyConfigProvider.setType({\n name: 'notificationSection',\n templateUrl: 'notificationRepeatSection.html',\n controller: function ($scope, NotificationService) {\n $scope.formOptions = {formState: $scope.formState};\n $scope.addNew = addNew;\n $scope.remove = remove;\n $scope.copyFields = copyFields;\n $scope.eventTypes = [];\n\n var allData = NotificationService.getAllData();\n _.each(_.keys(allData), function (key) {\n $scope.eventTypes.push({\"key\": key, \"label\": allData[key].readable})\n })\n\n function copyFields(fields) {\n fields = angular.copy(fields);\n $scope.repeatfields = fields;\n return fields;\n }\n\n $scope.clear = function (field) {\n return _.mapObject(field, function (key, val) {\n if (typeof val === 'object') {\n return $scope.clear(val);\n }\n return undefined;\n\n });\n };\n\n function addNew(eventType) {\n $scope.form.$setDirty(true);\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\n var repeatsection = $scope.model[$scope.options.key];\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\n\n var eventTypeData = NotificationService.getAllData()[eventType];\n console.log(eventTypeData);\n newsection.eventType = eventType;\n newsection.titleTemplate = eventTypeData.titleTemplate;\n newsection.bodyTemplate = eventTypeData.bodyTemplate;\n newsection.messageType = eventTypeData.messageType;\n\n repeatsection.push(newsection);\n }\n\n function remove($index) {\n $scope.model[$scope.options.key].splice($index, 1);\n $scope.form.$setDirty(true);\n }\n }\n });\n\n formlyConfigProvider.setType({\n //Button\n name: 'testNotification',\n templateUrl: 'button-test-notification.html',\n controller: function ($scope, NotificationService) {\n\n\n //When button is clicked\n $scope.testNotification = function () {\n NotificationService.testNotification($scope.model.eventType)\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalTestNotification',\n extends: 'testNotification',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n }]);\n\n","\nConfigService.$inject = [\"$http\", \"$q\", \"$cacheFactory\", \"$uibModal\", \"bootstrapped\", \"RequestsErrorHandler\"];angular\n .module('nzbhydraApp')\n .factory('ConfigService', ConfigService);\n\nfunction ConfigService($http, $q, $cacheFactory, $uibModal, bootstrapped, RequestsErrorHandler) {\n\n ConfigureInModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$http\", \"growl\", \"$interval\", \"RequestsErrorHandler\", \"localStorageService\", \"externalTool\", \"dialogInfo\"];\n var cache = $cacheFactory(\"nzbhydra\");\n var safeConfig = bootstrapped.safeConfig;\n\n return {\n set: set,\n get: get,\n getSafe: getSafe,\n invalidateSafe: invalidateSafe,\n maySeeAdminArea: maySeeAdminArea,\n reloadConfig: reloadConfig,\n apiHelp: apiHelp,\n configureIn: configureIn\n };\n\n function set(newConfig, ignoreWarnings) {\n var deferred = $q.defer();\n $http.put('internalapi/config', newConfig)\n .then(function (response) {\n if (response.data.ok && (ignoreWarnings || response.data.warningMessages.length === 0)) {\n cache.put(\"config\", newConfig);\n setTimeout(function () {\n invalidateSafe();\n }, 500)\n }\n deferred.resolve(response);\n\n }, function (errorresponse) {\n console.log(\"Error saving settings:\");\n console.log(errorresponse);\n deferred.reject(errorresponse);\n });\n return deferred.promise;\n }\n\n function reloadConfig() {\n return $http.get('internalapi/config/reload').then(function (response) {\n return response.data;\n });\n }\n\n function apiHelp() {\n return $http.get('internalapi/config/apiHelp').then(function (response) {\n return response.data;\n });\n }\n\n function get() {\n var config = cache.get(\"config\");\n if (angular.isUndefined(config)) {\n config = $http.get('internalapi/config').then(function (response) {\n return response.data;\n });\n cache.put(\"config\", config);\n }\n\n return config;\n }\n\n function getSafe() {\n return safeConfig;\n }\n\n function invalidateSafe() {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get('internalapi/config/safe').then(function (response) {\n safeConfig = response.data;\n });\n });\n\n }\n\n function maySeeAdminArea() {\n function loadAll() {\n var maySeeAdminArea = cache.get(\"maySeeAdminArea\");\n if (!angular.isUndefined(maySeeAdminArea)) {\n var deferred = $q.defer();\n deferred.resolve(maySeeAdminArea);\n return deferred.promise;\n }\n\n return $http.get('internalapi/mayseeadminarea')\n .then(function (configResponse) {\n var config = configResponse.data;\n cache.put(\"maySeeAdminArea\", config);\n return configResponse.data;\n });\n }\n\n return loadAll().then(function (maySeeAdminArea) {\n return maySeeAdminArea;\n });\n }\n\n function configureIn(externalTool) {\n $uibModal.open({\n templateUrl: 'static/html/configure-in-modal.html',\n controller: ConfigureInModalInstanceCtrl,\n size: \"md\",\n resolve: {\n externalTool: function () {\n return externalTool;\n },\n dialogInfo: function () {\n return $http.get(\"internalapi/externalTools/getDialogInfo\").then(function (response) {\n return response.data;\n })\n }\n }\n })\n }\n\n function ConfigureInModalInstanceCtrl($scope, $uibModalInstance, $http, growl, $interval, RequestsErrorHandler, localStorageService, externalTool, dialogInfo) {\n var lastConfig = localStorageService.get(externalTool);\n\n $scope.externalTool = externalTool;\n $scope.externalToolDisplayName = externalTool;\n $scope.externalToolsMessages = [];\n $scope.closeButtonType = \"warning\";\n $scope.completed = false;\n $scope.working = false;\n $scope.showMessages = false;\n\n $scope.nzbhydraHost = dialogInfo.nzbhydraHost;\n $scope.usenetIndexersConfigured = dialogInfo.usenetIndexersConfigured;\n $scope.prioritiesConfigured = dialogInfo.prioritiesConfigured;\n $scope.configureForUsenet = dialogInfo.usenetIndexersConfigured;\n $scope.torrentIndexersConfigured = dialogInfo.torrentIndexersConfigured;\n $scope.configureForTorrents = dialogInfo.torrentIndexersConfigured;\n $scope.addDisabledIndexers = false;\n\n if (!$scope.configureForUsenet && !$scope.configureForTorrents) {\n growl.error(\"No usenet or torrent indexers configured\");\n }\n\n $scope.nzbhydraName = \"NZBHydra2\";\n $scope.xdarrHost = \"http://localhost:\"\n $scope.addType = \"SINGLE\";\n $scope.enableRss = true;\n $scope.enableAutomaticSearch = true;\n $scope.enableInteractiveSearch = true;\n $scope.categories = null;\n $scope.animeCategories = null;\n $scope.priority = 0;\n $scope.useHydraPriorities = true;\n\n if (externalTool === \"Sonarr\" || externalTool === \"Sonarrv3\") {\n $scope.xdarrHost += \"8989\";\n $scope.categories = \"5030,5040\";\n if (externalTool === \"Sonarrv3\") {\n $scope.externalToolDisplayName = \"Sonarr v3\";\n }\n } else if (externalTool === \"Radarr\" || externalTool === \"Radarrv3\") {\n $scope.xdarrHost += \"7878\";\n $scope.categories = \"2000\";\n if (externalTool === \"Radarrv3\") {\n $scope.externalToolDisplayName = \"Radarr v3+\";\n }\n } else if (externalTool === \"Lidarr\") {\n $scope.xdarrHost += \"8686\";\n $scope.categories = \"3000\";\n } else if (externalTool === \"Readarr\") {\n $scope.xdarrHost += \"8787\";\n $scope.categories = \"7020,8010\";\n }\n $scope.removeYearFromSearchString = false;\n\n if (lastConfig !== null && lastConfig !== undefined) {\n Object.assign($scope, lastConfig);\n }\n\n $scope.close = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.submit = function (deleteOnly) {\n if ($scope.completed && !deleteOnly) {\n $uibModalInstance.dismiss();\n }\n if (!$scope.usenetIndexersConfigured && !$scope.torrentIndexersConfigured && !deleteOnly) {\n growl.error(\"No usenet or torrent indexers configured\");\n return;\n }\n $scope.externalToolsMessages = [];\n $scope.spinnerActive = true;\n $scope.working = true;\n $scope.showMessages = true;\n var data = {\n\n nzbhydraName: $scope.nzbhydraName,\n externalTool: $scope.externalTool,\n nzbhydraHost: $scope.nzbhydraHost,\n addType: deleteOnly ? \"DELETE_ONLY\" : $scope.addType,\n xdarrHost: $scope.xdarrHost,\n xdarrApiKey: $scope.xdarrApiKey,\n enableRss: $scope.enableRss,\n enableAutomaticSearch: $scope.enableAutomaticSearch,\n enableInteractiveSearch: $scope.enableInteractiveSearch,\n categories: $scope.categories,\n animeCategories: $scope.animeCategories,\n removeYearFromSearchString: $scope.removeYearFromSearchString,\n earlyDownloadLimit: $scope.earlyDownloadLimit,\n multiLanguages: $scope.multiLanguages,\n configureForUsenet: $scope.configureForUsenet,\n configureForTorrents: $scope.configureForTorrents,\n additionalParameters: $scope.additionalParameters,\n minimumSeeders: $scope.minimumSeeders,\n seedRatio: $scope.seedRatio,\n seedTime: $scope.seedTime,\n seasonPackSeedTime: $scope.seasonPackSeedTime,\n discographySeedTime: $scope.discographySeedTime,\n addDisabledIndexers: $scope.addDisabledIndexers,\n priority: $scope.priority,\n useHydraPriorities: $scope.useHydraPriorities\n }\n\n localStorageService.set(externalTool, data);\n\n function updateMessages() {\n $http.get(\"internalapi/externalTools/messages\").then(function (response) {\n $scope.externalToolsMessages = response.data;\n });\n }\n\n var updateInterval = $interval(function () {\n updateMessages();\n }, 500);\n\n RequestsErrorHandler.specificallyHandled(function () {\n $scope.completed = false;\n $http.post(\"internalapi/externalTools/configure\", data).then(function (response) {\n updateMessages();\n $interval.cancel(updateInterval);\n $scope.spinnerActive = false;\n console.log(response);\n if (response.data) {\n $scope.completed = true;\n $scope.closeButtonType = \"success\";\n } else {\n $scope.working = false;\n $scope.completed = false;\n }\n }, function (error) {\n updateMessages();\n console.error(error.data);\n $interval.cancel(updateInterval);\n $scope.completed = false;\n $scope.spinnerActive = false;\n $scope.working = false;\n });\n });\n };\n\n }\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nConfigFields.$inject = [\"$injector\"];\nangular\n .module('nzbhydraApp')\n .factory('ConfigFields', ConfigFields);\n\nfunction ConfigFields($injector) {\n return {\n getFields: getFields\n };\n\n function ipValidator() {\n return {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n return /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(value)\n || /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(value);\n }\n return true;\n },\n message: '$viewValue + \" is not a valid IP Address\"'\n };\n }\n\n function regexValidator(regex, message, prefixViewValue, preventEmpty) {\n return {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n if (Array.isArray(value)) {\n for (var i = 0; i < value.length; i++) {\n if (!regex.test(value[i])) {\n return false;\n }\n }\n return true;\n } else {\n return regex.test(value);\n }\n }\n return !preventEmpty;\n },\n message: (prefixViewValue ? '$viewValue + \" ' : '\" ') + message + '\"'\n };\n }\n\n function getFields(rootModel, showAdvanced) {\n return {\n main: [\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Hosting'},\n fieldGroup: [\n {\n key: 'host',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Host',\n required: true,\n placeholder: 'IPv4 address to bind to',\n help: 'I strongly recommend using a reverse proxy instead of exposing this directly. Requires restart.'\n },\n validators: {\n ipAddress: ipValidator()\n }\n },\n {\n key: 'port',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Port',\n required: true,\n placeholder: '5076',\n help: 'Requires restart.'\n },\n validators: {\n port: regexValidator(/^\\d{1,5}$/, \"is no valid port\", true)\n }\n },\n {\n key: 'urlBase',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'URL base',\n placeholder: '/nzbhydra',\n help: 'Adapt when using a reverse proxy. See wiki. Always use when calling Hydra, even locally.',\n tooltip: 'If you use Hydra behind a reverse proxy you might want to set the URL base to a value like \"/nzbhydra\". If you accesses Hydra with tools running outside your network (for example from your phone) set the external URL so that it matches the full Hydra URL. That way the NZB links returned in the search results refer to your global URL and not your local address.',\n advanced: true\n },\n validators: {\n urlBase: regexValidator(/^((\\/.*[^\\/])|\\/)$/, 'URL base has to start and may not end with /', false, true)\n }\n\n },\n {\n key: 'ssl',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Use SSL',\n help: 'Requires restart.',\n tooltip: 'You can use SSL but I recommend using a reverse proxy with SSL. See the wiki for notes regarding reverse proxies and SSL. It\\'s more secure and can be configured better.',\n advanced: true\n }\n },\n {\n key: 'sslKeyStore',\n hideExpression: '!model.ssl',\n type: 'fileInput',\n templateOptions: {\n label: 'SSL keystore file',\n required: true,\n type: \"file\",\n help: 'Requires restart. See wiki.'\n }\n },\n {\n key: 'sslKeyStorePassword',\n hideExpression: '!model.ssl',\n type: 'horizontalInput',\n templateOptions: {\n type: 'password',\n label: 'SSL keystore password',\n required: true,\n help: 'Requires restart.'\n }\n }\n\n\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Proxy',\n tooltip: 'You can select to use either a SOCKS or an HTTPS proxy. All outside connections will be done via the configured proxy.',\n advanced: true\n }\n ,\n fieldGroup: [\n {\n key: 'proxyType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Use proxy',\n options: [\n {name: 'None', value: 'NONE'},\n {name: 'SOCKS', value: 'SOCKS'},\n {name: 'HTTP(S)', value: 'HTTP'}\n ]\n }\n },\n {\n key: 'proxyHost',\n type: 'horizontalInput',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n label: 'SOCKS proxy host',\n placeholder: 'Set to use a SOCKS proxy',\n help: \"IPv4 only\"\n }\n },\n {\n key: 'proxyPort',\n type: 'horizontalInput',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'number',\n label: 'Proxy port',\n placeholder: '1080'\n }\n },\n {\n key: 'proxyUsername',\n type: 'horizontalInput',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n label: 'Proxy username'\n }\n },\n {\n key: 'proxyPassword',\n type: 'passwordSwitch',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n label: 'Proxy password'\n }\n },\n {\n key: 'proxyIgnoreLocal',\n type: 'horizontalSwitch',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'switch',\n label: 'Bypass local network addresses'\n }\n },\n {\n key: 'proxyIgnoreDomains',\n type: 'horizontalChips',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n help: 'Separate by comma. You can use wildcards (*). Case insensitive. Apply values with enter key.',\n label: 'Bypass domains'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'UI'},\n fieldGroup: [\n\n {\n key: 'theme',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Theme',\n options: [\n {name: 'Grey', value: 'grey'},\n {name: 'Bright', value: 'bright'},\n {name: 'Dark', value: 'dark'}\n ]\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Security'},\n fieldGroup: [\n {\n key: 'apiKey',\n type: 'horizontalApiKeyInput',\n templateOptions: {\n label: 'API key',\n help: 'Alphanumeric only.',\n required: true\n },\n validators: {\n apiKey: regexValidator(/^[a-zA-Z0-9]*$/, \"API key must only contain numbers and digits\", false)\n }\n },\n {\n key: 'dereferer',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Dereferer',\n help: 'Redirect external links to hide your instance. Insert $s for escaped target URL and $us for unescaped target URL. Use empty value to disable.',\n advanced: true\n }\n },\n {\n key: 'verifySsl',\n type: 'horizontalSwitch',\n templateOptions: {\n label: 'Verify SSL certificates',\n help: 'If enabled only valid/known SSL certificates will be accepted when accessing indexers. Change requires restart. See wiki.',\n advanced: true\n }\n },\n {\n key: 'verifySslDisabledFor',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Disable SSL for...',\n help: 'Add hosts for which to disable SSL verification. Apply words with return key.',\n advanced: true\n }\n },\n {\n key: 'disableSslLocally',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'text',\n label: 'Disable SSL locally',\n help: 'Disable SSL for local hosts.',\n advanced: true\n }\n },\n {\n key: 'sniDisabledFor',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Disable SNI',\n help: 'Add a host if you get an \"unrecognized_name\" error. Apply words with return key. See wiki.',\n advanced: true\n }\n },\n {\n key: 'useCsrf',\n type: 'horizontalSwitch',\n templateOptions: {\n label: 'Use CSRF protection',\n help: 'Use CSRF protection.',\n advanced: true\n }\n }\n ]\n },\n\n {\n wrapper: 'fieldset',\n key: 'logging',\n templateOptions: {\n label: 'Logging',\n tooltip: 'The base settings should suffice for most users. If you want you can enable logging of IP adresses for failed logins and NZB downloads.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'logfilelevel',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Logfile level',\n options: [\n {name: 'Error', value: 'ERROR'},\n {name: 'Warning', value: 'WARN'},\n {name: 'Info', value: 'INFO'},\n {name: 'Debug', value: 'DEBUG'}\n ],\n help: 'Takes effect on next restart.'\n }\n },\n {\n key: 'logMaxHistory',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Max log history',\n help: 'How many daily log files will be kept.'\n }\n },\n {\n key: 'consolelevel',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Console log level',\n options: [\n {name: 'Error', value: 'ERROR'},\n {name: 'Warning', value: 'WARN'},\n {name: 'Info', value: 'INFO'},\n {name: 'Debug', value: 'DEBUG'}\n ],\n help: 'Takes effect on next restart.'\n }\n },\n {\n key: 'logGc',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Log GC',\n help: 'Enable garbage collection logging. Only for debugging of memory issues.'\n }\n },\n {\n key: 'logIpAddresses',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Log IP addresses'\n }\n },\n {\n key: 'mapIpToHost',\n type: 'horizontalSwitch',\n hideExpression: '!model.logIpAddresses',\n templateOptions: {\n type: 'switch',\n label: 'Map hosts',\n help: 'Try to map logged IP addresses to host names.',\n tooltip: 'Enabling this may cause NZBHydra to load very, very slowly when accessed remotely.'\n }\n },\n {\n key: 'logUsername',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Log user names'\n }\n },\n {\n key: 'markersToLog',\n type: 'horizontalMultiselect',\n hideExpression: 'model.consolelevel !== \"DEBUG\" && model.logfilelevel !== \"DEBUG\"',\n templateOptions: {\n label: 'Log markers',\n help: 'Select certain sections for more output on debug level. Please enable only when asked for.',\n options: [\n {label: 'API limits', id: 'LIMITS'},\n {label: 'Category mapping', id: 'CATEGORY_MAPPING'},\n {label: 'Config file handling', id: 'CONFIG_READ_WRITE'},\n {label: 'Custom mapping', id: 'CUSTOM_MAPPING'},\n {label: 'Downloader status updating', id: 'DOWNLOADER_STATUS_UPDATE'},\n {label: 'Duplicate detection', id: 'DUPLICATES'},\n {label: 'External tool configuration', id: 'EXTERNAL_TOOLS'},\n {label: 'History cleanup', id: 'HISTORY_CLEANUP'},\n {label: 'HTTP', id: 'HTTP'},\n {label: 'HTTPS', id: 'HTTPS'},\n {label: 'HTTP Server', id: 'SERVER'},\n {label: 'Indexer scheduler', id: 'SCHEDULER'},\n {label: 'Notifications', id: 'NOTIFICATIONS'},\n {label: 'NZB download status updating', id: 'DOWNLOAD_STATUS_UPDATE'},\n {label: 'Performance', id: 'PERFORMANCE'},\n {label: 'Rejected results', id: 'RESULT_ACCEPTOR'},\n {label: 'Removed trailing words', id: 'TRAILING'},\n {label: 'URL calculation', id: 'URL_CALCULATION'},\n {label: 'User agent mapping', id: 'USER_AGENT'},\n {label: 'VIP expiry', id: 'VIP_EXPIRY'}\n ],\n buttonText: \"None\"\n }\n },\n {\n key: 'historyUserInfoType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'History user info',\n options: [\n {name: 'IP and username', value: 'BOTH'},\n {name: 'IP address', value: 'IP'},\n {name: 'Username', value: 'USERNAME'},\n {name: 'None', value: 'NONE'}\n ],\n help: 'Only affects if value is displayed in the search/download history.',\n hideExpression: '!model.keepHistory'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Backup',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'backupFolder',\n type: 'horizontalInput',\n templateOptions: {\n label: 'Backup folder',\n help: 'Either relative to the NZBHydra data folder or an absolute folder.'\n }\n },\n {\n key: 'backupEveryXDays',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Backup every...',\n addonRight: {\n text: 'days'\n }\n }\n },\n {\n key: 'backupBeforeUpdate',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Backup before update'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Updates'},\n fieldGroup: [\n {\n key: 'updateAutomatically',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Install updates automatically'\n }\n }, {\n key: 'updateToPrereleases',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Install prereleases',\n advanced: true\n }\n },\n {\n key: 'deleteBackupsAfterWeeks',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Delete backups after...',\n addonRight: {\n text: 'weeks'\n },\n advanced: true\n }\n },\n {\n key: 'showUpdateBannerOnDocker',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show update banner when managed externally',\n advanced: true,\n help: 'If enabled a banner will be shown when new versions are available even when NZBHydra is run inside docker or is installed using a package manager (where you wouldn\\'t let NZBHydra update itself).'\n }\n },\n {\n key: 'showWhatsNewBanner',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show info banner after automatic updates',\n help: 'Please keep it enabled, I put some effort into the changelog ;-)',\n advanced: true\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'History',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'keepHistory',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Keep history',\n help: 'Controls search and download history.',\n tooltip: 'If disabled no search or download history will be kept. These sections will be hidden in the GUI. You won\\'t be able to see stats. The database will still contain a short-lived history of transactions that are kept for 24 hours.'\n }\n },\n {\n key: 'keepHistoryForWeeks',\n type: 'horizontalInput',\n hideExpression: '!model.keepHistory',\n templateOptions: {\n type: 'number',\n label: 'Keep history for...',\n addonRight: {\n text: 'weeks'\n },\n min: 1,\n help: 'Only keep history (searches, downloads) for a certain time. Will decrease database size and may improve performance a bit. Rather reduce how long stats are kept.'\n }\n },\n {\n key: 'keepStatsForWeeks',\n type: 'horizontalInput',\n hideExpression: '!model.keepHistory',\n templateOptions: {\n type: 'number',\n label: 'Keep stats for...',\n addonRight: {\n text: 'weeks'\n },\n min: 1,\n help: 'Only keep stats for a certain time. Will decrease database size.'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Database',\n tooltip: 'You should not change these values unless you\\'re either told to or really know what you\\'re doing.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'databaseCompactTime',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Database compact time',\n addonRight: {\n text: 'ms'\n },\n min: 200,\n help: 'The time the database is given to compact (reduce size) when shutting down. Reduce this if shutting down NZBHydra takes too long (database size may increase). Takes effect on next restart.'\n }\n },\n {\n key: 'databaseRetentionTime',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Database retention time',\n addonRight: {\n text: 'ms'\n },\n help: 'How long the db should retain old, persisted data. See here.'\n }\n },\n {\n key: 'databaseWriteDelay',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Database write delay',\n addonRight: {\n text: 'ms'\n },\n help: 'Maximum delay between a commit and flushing the log, in milliseconds. See here.'\n }\n }\n\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Other'},\n fieldGroup: [\n {\n key: 'startupBrowser',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Open browser on startup'\n }\n },\n {\n key: 'showNews',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show news',\n help: \"Hydra will occasionally show news when opened. You can always find them in the system section\",\n advanced: true\n }\n },\n {\n key: 'xmx',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'JVM memory',\n addonRight: {\n text: 'MB'\n },\n min: 128,\n help: '256 should suffice except when working with big databases / many indexers. See wiki.',\n advanced: true\n }\n }\n ]\n\n }\n ],\n\n searching: [\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Indexer access',\n tooltip: 'Settings that control how communication with indexers is done and how to handle errors while doing that.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'timeout',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Timeout when accessing indexers',\n help: 'Any web call to an indexer taking longer than this is aborted.',\n min: 1,\n addonRight: {\n text: 'seconds'\n }\n }\n },\n {\n key: 'userAgent',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'User agent',\n help: 'Used when accessing indexers.',\n required: true,\n tooltip: 'Some indexers don\\'t seem to like Hydra and disable access based on the user agent. You can change it here if you want. Please leave it as it is if you have no problems. This allows indexers to gather better statistics on how their API services are used.',\n }\n },\n {\n key: 'userAgents',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Map user agents',\n help: 'Used to map the user agent from accessing services to the service names. Apply words with return key.',\n }\n },\n {\n key: 'ignoreLoadLimitingForInternalSearches',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Ignore load limiting internally',\n help: 'When enabled load limiting defined for indexers will be ignored for internal searches.',\n }\n },\n {\n key: 'ignoreTemporarilyDisabled',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Ignore temporary errors',\n tooltip: \"By default if access to an indexer fails the indexer is disabled for a certain amount of time (for a short while first, then increasingly longer if the problems persist). Disable this and always try these indexers.\",\n }\n }\n ]\n }, {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Category handling',\n tooltip: 'Settings that control the handling of newznab categories (e.g. 2000 for Movies).',\n advanced: true\n },\n fieldGroup: [\n\n {\n key: 'transformNewznabCategories',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Transform newznab categories',\n help: 'Map newznab categories from API searches to configured categories and use all configured newznab categories in searches.'\n }\n },\n {\n key: 'sendTorznabCategories',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Send categories to trackers',\n help: 'If disabled no categories will be included in queries to torznab indexers (trackers).'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Media IDs / Query generation / Query processing',\n tooltip: 'Raw search engines like Binsearch don\\'t support searches based on IDs (e.g. for a movie using an IMDB id). You can enable query generation for these. Hydra will then try to retrieve the movie\\'s or show\\'s title and generate a query, for example \"showname s01e01\". In some cases an ID based search will not provide any results. You can enable a fallback so that in such a case the search will be repeated with a query using the title of the show or movie.'\n },\n fieldGroup: [\n {\n key: 'alwaysConvertIds',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Convert media IDs for...',\n options: [\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'All searches', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"When enabled media ID conversions will always be done even when an indexer supports the already known ID(s).\",\n advanced: true\n }\n },\n {\n key: 'generateQueries',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Generate queries',\n options: [\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'All searches', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"Generate queries for indexers which do not support ID based searches.\"\n }\n },\n {\n key: 'idFallbackToQueryGeneration',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Fallback to generated queries',\n options: [\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'All searches', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"When no results were found for a query ID search again using a generated query (on indexer level).\"\n }\n },\n {\n key: 'language',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'text',\n label: 'Language',\n required: true,\n help: 'Used for movie query generation and autocomplete only.',\n options: [{\"name\": \"Abkhaz\", value: \"ab\"}, {\n \"name\": \"Afar\",\n value: \"aa\"\n }, {\"name\": \"Afrikaans\", value: \"af\"}, {\"name\": \"Akan\", value: \"ak\"}, {\n \"name\": \"Albanian\",\n value: \"sq\"\n }, {\"name\": \"Amharic\", value: \"am\"}, {\n \"name\": \"Arabic\",\n value: \"ar\"\n }, {\"name\": \"Aragonese\", value: \"an\"}, {\"name\": \"Armenian\", value: \"hy\"}, {\n \"name\": \"Assamese\",\n value: \"as\"\n }, {\"name\": \"Avaric\", value: \"av\"}, {\"name\": \"Avestan\", value: \"ae\"}, {\n \"name\": \"Aymara\",\n value: \"ay\"\n }, {\"name\": \"Azerbaijani\", value: \"az\"}, {\n \"name\": \"Bambara\",\n value: \"bm\"\n }, {\"name\": \"Bashkir\", value: \"ba\"}, {\n \"name\": \"Basque\",\n value: \"eu\"\n }, {\"name\": \"Belarusian\", value: \"be\"}, {\"name\": \"Bengali\", value: \"bn\"}, {\n \"name\": \"Bihari\",\n value: \"bh\"\n }, {\"name\": \"Bislama\", value: \"bi\"}, {\n \"name\": \"Bosnian\",\n value: \"bs\"\n }, {\"name\": \"Breton\", value: \"br\"}, {\"name\": \"Bulgarian\", value: \"bg\"}, {\n \"name\": \"Burmese\",\n value: \"my\"\n }, {\"name\": \"Catalan\", value: \"ca\"}, {\n \"name\": \"Chamorro\",\n value: \"ch\"\n }, {\"name\": \"Chechen\", value: \"ce\"}, {\"name\": \"Chichewa\", value: \"ny\"}, {\n \"name\": \"Chinese\",\n value: \"zh\"\n }, {\"name\": \"Chuvash\", value: \"cv\"}, {\n \"name\": \"Cornish\",\n value: \"kw\"\n }, {\"name\": \"Corsican\", value: \"co\"}, {\"name\": \"Cree\", value: \"cr\"}, {\n \"name\": \"Croatian\",\n value: \"hr\"\n }, {\"name\": \"Czech\", value: \"cs\"}, {\"name\": \"Danish\", value: \"da\"}, {\n \"name\": \"Divehi\",\n value: \"dv\"\n }, {\"name\": \"Dutch\", value: \"nl\"}, {\n \"name\": \"Dzongkha\",\n value: \"dz\"\n }, {\"name\": \"English\", value: \"en\"}, {\n \"name\": \"Esperanto\",\n value: \"eo\"\n }, {\"name\": \"Estonian\", value: \"et\"}, {\"name\": \"Ewe\", value: \"ee\"}, {\n \"name\": \"Faroese\",\n value: \"fo\"\n }, {\"name\": \"Fijian\", value: \"fj\"}, {\"name\": \"Finnish\", value: \"fi\"}, {\n \"name\": \"French\",\n value: \"fr\"\n }, {\"name\": \"Fula\", value: \"ff\"}, {\n \"name\": \"Galician\",\n value: \"gl\"\n }, {\"name\": \"Georgian\", value: \"ka\"}, {\"name\": \"German\", value: \"de\"}, {\n \"name\": \"Greek\",\n value: \"el\"\n }, {\"name\": \"Guaraní\", value: \"gn\"}, {\n \"name\": \"Gujarati\",\n value: \"gu\"\n }, {\"name\": \"Haitian\", value: \"ht\"}, {\"name\": \"Hausa\", value: \"ha\"}, {\n \"name\": \"Hebrew\",\n value: \"he\"\n }, {\"name\": \"Herero\", value: \"hz\"}, {\n \"name\": \"Hindi\",\n value: \"hi\"\n }, {\"name\": \"Hiri Motu\", value: \"ho\"}, {\n \"name\": \"Hungarian\",\n value: \"hu\"\n }, {\"name\": \"Interlingua\", value: \"ia\"}, {\n \"name\": \"Indonesian\",\n value: \"id\"\n }, {\"name\": \"Interlingue\", value: \"ie\"}, {\n \"name\": \"Irish\",\n value: \"ga\"\n }, {\"name\": \"Igbo\", value: \"ig\"}, {\"name\": \"Inupiaq\", value: \"ik\"}, {\n \"name\": \"Ido\",\n value: \"io\"\n }, {\"name\": \"Icelandic\", value: \"is\"}, {\n \"name\": \"Italian\",\n value: \"it\"\n }, {\"name\": \"Inuktitut\", value: \"iu\"}, {\"name\": \"Japanese\", value: \"ja\"}, {\n \"name\": \"Javanese\",\n value: \"jv\"\n }, {\"name\": \"Kalaallisut\", value: \"kl\"}, {\n \"name\": \"Kannada\",\n value: \"kn\"\n }, {\"name\": \"Kanuri\", value: \"kr\"}, {\"name\": \"Kashmiri\", value: \"ks\"}, {\n \"name\": \"Kazakh\",\n value: \"kk\"\n }, {\"name\": \"Khmer\", value: \"km\"}, {\n \"name\": \"Kikuyu\",\n value: \"ki\"\n }, {\"name\": \"Kinyarwanda\", value: \"rw\"}, {\"name\": \"Kyrgyz\", value: \"ky\"}, {\n \"name\": \"Komi\",\n value: \"kv\"\n }, {\"name\": \"Kongo\", value: \"kg\"}, {\"name\": \"Korean\", value: \"ko\"}, {\n \"name\": \"Kurdish\",\n value: \"ku\"\n }, {\"name\": \"Kwanyama\", value: \"kj\"}, {\n \"name\": \"Latin\",\n value: \"la\"\n }, {\"name\": \"Luxembourgish\", value: \"lb\"}, {\n \"name\": \"Ganda\",\n value: \"lg\"\n }, {\"name\": \"Limburgish\", value: \"li\"}, {\"name\": \"Lingala\", value: \"ln\"}, {\n \"name\": \"Lao\",\n value: \"lo\"\n }, {\"name\": \"Lithuanian\", value: \"lt\"}, {\n \"name\": \"Luba-Katanga\",\n value: \"lu\"\n }, {\"name\": \"Latvian\", value: \"lv\"}, {\"name\": \"Manx\", value: \"gv\"}, {\n \"name\": \"Macedonian\",\n value: \"mk\"\n }, {\"name\": \"Malagasy\", value: \"mg\"}, {\n \"name\": \"Malay\",\n value: \"ms\"\n }, {\"name\": \"Malayalam\", value: \"ml\"}, {\"name\": \"Maltese\", value: \"mt\"}, {\n \"name\": \"Māori\",\n value: \"mi\"\n }, {\"name\": \"Marathi\", value: \"mr\"}, {\n \"name\": \"Marshallese\",\n value: \"mh\"\n }, {\"name\": \"Mongolian\", value: \"mn\"}, {\"name\": \"Nauru\", value: \"na\"}, {\n \"name\": \"Navajo\",\n value: \"nv\"\n }, {\"name\": \"Northern Ndebele\", value: \"nd\"}, {\n \"name\": \"Nepali\",\n value: \"ne\"\n }, {\"name\": \"Ndonga\", value: \"ng\"}, {\n \"name\": \"Norwegian Bokmål\",\n value: \"nb\"\n }, {\"name\": \"Norwegian Nynorsk\", value: \"nn\"}, {\n \"name\": \"Norwegian\",\n value: \"no\"\n }, {\"name\": \"Nuosu\", value: \"ii\"}, {\n \"name\": \"Southern Ndebele\",\n value: \"nr\"\n }, {\"name\": \"Occitan\", value: \"oc\"}, {\n \"name\": \"Ojibwe\",\n value: \"oj\"\n }, {\"name\": \"Old Church Slavonic\", value: \"cu\"}, {\"name\": \"Oromo\", value: \"om\"}, {\n \"name\": \"Oriya\",\n value: \"or\"\n }, {\"name\": \"Ossetian\", value: \"os\"}, {\"name\": \"Panjabi\", value: \"pa\"}, {\n \"name\": \"Pāli\",\n value: \"pi\"\n }, {\"name\": \"Persian\", value: \"fa\"}, {\n \"name\": \"Polish\",\n value: \"pl\"\n }, {\"name\": \"Pashto\", value: \"ps\"}, {\n \"name\": \"Portuguese\",\n value: \"pt\"\n }, {\"name\": \"Quechua\", value: \"qu\"}, {\"name\": \"Romansh\", value: \"rm\"}, {\n \"name\": \"Kirundi\",\n value: \"rn\"\n }, {\"name\": \"Romanian\", value: \"ro\"}, {\n \"name\": \"Russian\",\n value: \"ru\"\n }, {\"name\": \"Sanskrit\", value: \"sa\"}, {\"name\": \"Sardinian\", value: \"sc\"}, {\n \"name\": \"Sindhi\",\n value: \"sd\"\n }, {\"name\": \"Northern Sami\", value: \"se\"}, {\n \"name\": \"Samoan\",\n value: \"sm\"\n }, {\"name\": \"Sango\", value: \"sg\"}, {\"name\": \"Serbian\", value: \"sr\"}, {\n \"name\": \"Gaelic\",\n value: \"gd\"\n }, {\"name\": \"Shona\", value: \"sn\"}, {\"name\": \"Sinhala\", value: \"si\"}, {\n \"name\": \"Slovak\",\n value: \"sk\"\n }, {\"name\": \"Slovene\", value: \"sl\"}, {\n \"name\": \"Somali\",\n value: \"so\"\n }, {\"name\": \"Southern Sotho\", value: \"st\"}, {\n \"name\": \"Spanish\",\n value: \"es\"\n }, {\"name\": \"Sundanese\", value: \"su\"}, {\"name\": \"Swahili\", value: \"sw\"}, {\n \"name\": \"Swati\",\n value: \"ss\"\n }, {\"name\": \"Swedish\", value: \"sv\"}, {\"name\": \"Tamil\", value: \"ta\"}, {\n \"name\": \"Telugu\",\n value: \"te\"\n }, {\"name\": \"Tajik\", value: \"tg\"}, {\n \"name\": \"Thai\",\n value: \"th\"\n }, {\"name\": \"Tigrinya\", value: \"ti\"}, {\n \"name\": \"Tibetan Standard\",\n value: \"bo\"\n }, {\"name\": \"Turkmen\", value: \"tk\"}, {\"name\": \"Tagalog\", value: \"tl\"}, {\n \"name\": \"Tswana\",\n value: \"tn\"\n }, {\"name\": \"Tonga\", value: \"to\"}, {\"name\": \"Turkish\", value: \"tr\"}, {\n \"name\": \"Tsonga\",\n value: \"ts\"\n }, {\"name\": \"Tatar\", value: \"tt\"}, {\n \"name\": \"Twi\",\n value: \"tw\"\n }, {\"name\": \"Tahitian\", value: \"ty\"}, {\n \"name\": \"Uyghur\",\n value: \"ug\"\n }, {\"name\": \"Ukrainian\", value: \"uk\"}, {\"name\": \"Urdu\", value: \"ur\"}, {\n \"name\": \"Uzbek\",\n value: \"uz\"\n }, {\"name\": \"Venda\", value: \"ve\"}, {\n \"name\": \"Vietnamese\",\n value: \"vi\"\n }, {\"name\": \"Volapük\", value: \"vo\"}, {\"name\": \"Walloon\", value: \"wa\"}, {\n \"name\": \"Welsh\",\n value: \"cy\"\n }, {\"name\": \"Wolof\", value: \"wo\"}, {\n \"name\": \"Western Frisian\",\n value: \"fy\"\n }, {\"name\": \"Xhosa\", value: \"xh\"}, {\"name\": \"Yiddish\", value: \"yi\"}, {\n \"name\": \"Yoruba\",\n value: \"yo\"\n }, {\"name\": \"Zhuang\", value: \"za\"}, {\"name\": \"Zulu\", value: \"zu\"}]\n }\n },\n {\n key: 'replaceUmlauts',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Replace umlauts',\n help: 'Replace german umlauts and special characters (ä, ö, ü and ß) in external request queries.'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Result filters',\n tooltip: 'This section allows you to define global filters which will be applied to all search results. You can define words and regexes which must or must not be matched for a search result to be matched. You can also exclude certain usenet posters and groups which are known for spamming. You can define forbidden and required words for categories in the next tab (Categories). Usually required or forbidden words are applied on a word base, so they must form a complete word in a title. Only if they contain a dash or a dot they may appear anywhere in the title. Example: \"ea\" matches \"something.from.ea\" but not \"release.from.other\". \"web-dl\" matches \"title.web-dl\" and \"someweb-dl\".'\n },\n fieldGroup: [\n {\n key: 'applyRestrictions',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Apply word filters',\n options: [\n {name: 'All searches', value: 'BOTH'},\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"For which type of search word/regex filters will be applied\"\n }\n },\n {\n key: 'forbiddenWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden words',\n help: \"Results with any of these words in the title will be ignored. Title is converted to lowercase before. Apply words with return key.\",\n tooltip: 'One forbidden word in a result title dismisses the result.'\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'forbiddenRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Forbidden regex',\n help: 'Must not be present in a title (case is ignored).',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'requiredWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Required words',\n help: \"Only results with titles that contain *all* words will be used. Title is converted to lowercase before. Apply words with return key.\",\n tooltip: 'If any of the required words is not found anywhere in a result title it\\'s also dismissed.'\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'requiredRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Required regex',\n help: 'Must be present in a title (case is ignored).',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n\n {\n key: 'forbiddenGroups',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden groups',\n help: 'Posts from any groups containing any of these words will be ignored. Apply words with return key.',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'forbiddenPosters',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden posters',\n help: 'Posts from any posters containing any of these words will be ignored. Apply words with return key.',\n advanced: true\n }\n },\n {\n key: 'languagesToKeep',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Languages to keep',\n help: 'If an indexer returns the language in the results only those results with configured languages will be used. Apply words with return key.'\n }\n },\n {\n key: 'maxAge',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Maximum results age',\n help: 'Results older than this are ignored. Can be overwritten per search. Apply words with return key.',\n addonRight: {\n text: 'days'\n }\n }\n },\n {\n key: 'minSeeders',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Minimum # seeders',\n help: 'Torznab results with fewer seeders will be ignored.'\n }\n },\n {\n key: 'ignorePassworded',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Ignore passworded releases',\n help: \"Not all indexers provide this information\",\n tooltip: 'Some indexers provide information if a release is passworded. If you select to ignore these releases only those will be ignored of which I know for sure that they\\'re actually passworded.'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Result processing'\n },\n fieldGroup: [\n {\n key: 'wrapApiErrors',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'text',\n label: 'Wrap API errors in empty results page',\n help: 'When enabled accessing tools will think the search was completed successfully but without results.',\n tooltip: 'In (hopefully) rare cases Hydra may crash when processing an API search request. You can enable to return an empty search page in these cases (if Hydra hasn\\'t crashed altogether ). This means that the calling tool (e.g. Sonarr) will think that the indexer (Hydra) is fine but just didn\\'t return a result. That way Hydra won\\'t be disabled as indexer but on the downside you may not be directly notified that an error occurred.',\n advanced: true\n }\n },\n {\n key: 'removeTrailing',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Remove trailing...',\n help: 'Removed from title if it ends with either of these. Case insensitive and disregards leading/trailing spaces. Allows wildcards (\"*\"). Apply words with return key.',\n tooltip: 'Hydra contains a predefined list of words which will be removed if a search result title ends with them. This allows better duplicate detection and cleans up the titles. Trailing words will be removed until none of the defined strings are found at the end of the result title.'\n }\n },\n {\n key: 'useOriginalCategories',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Use original categories',\n help: 'Enable to use the category descriptions provided by the indexer.',\n tooltip: 'Hydra attempts to parse the provided newznab category IDs for results and map them to the configured categories. In some cases this may lead to category names which are not quite correct. You can select to use the original category name used by the indexer. This will only affect which category name is shown in the results.',\n advanced: true\n }\n }\n ]\n },\n {\n type: 'repeatSection',\n key: 'customMappings',\n model: rootModel.searching,\n templateOptions: {\n tooltip: 'Here you can define mappings to modify either queries or titles for search requests or to dynamically change the titles of found results. The former allows you, for example, to change requests made by external tools, the latter to clean up results by indexers in a more advanced way.',\n btnText: 'Add new custom mapping',\n altLegendText: 'Mapping',\n headline: 'Custom mappings of queries, search titles and result titles',\n advanced: true,\n fields: [\n {\n key: 'affectedValue',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Affected value',\n options: [\n {name: 'Query', value: 'QUERY'},\n {name: 'Search title', value: 'TITLE'},\n {name: 'Result title', value: 'RESULT_TITLE'},\n ],\n required: true,\n help: \"Determines which value of the search request or result will be processed\"\n }\n },\n {\n key: 'searchType',\n type: 'horizontalSelect',\n hideExpression: 'model.affectedValue === \"RESULT_TITLE\"',\n templateOptions: {\n label: 'Search type',\n options: [\n {name: 'General', value: 'SEARCH'},\n {name: 'Audio', value: 'MUSIC'},\n {name: 'EBook', value: 'BOOK'},\n {name: 'Movie', value: 'MOVIE'},\n {name: 'TV', value: 'TVSEARCH'}\n ],\n help: \"Determines in what context the mapping will be executed\"\n }\n },\n {\n key: 'from',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Input pattern',\n help: 'Pattern which must match the query or title of a search request. You may use regexes in groups which can be referenced in the output puttern by using {group:regex}. Case insensitive.',\n required: true\n }\n },\n {\n key: 'to',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Output pattern',\n help: 'If a query or title matches the input pattern it will be replaced using this. You may reference groups from the input pattern by using {group}. Additionally you may use {season:0} or {season:00} or {episode:0} or {episode:00} (with and without leading zeroes).',\n required: true\n }\n },\n {\n type: 'customMappingTest',\n }\n ],\n defaultModel: {\n searchType: null,\n affectedValue: null,\n from: null,\n to: null\n }\n }\n },\n\n\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Result display'\n },\n fieldGroup: [\n {\n key: 'loadAllCachedOnInternal',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Display all retrieved results',\n help: 'Load all results already retrieved from indexers. Might make sorting / filtering a bit slower. Will still be paged according to the limit set above.',\n advanced: true\n }\n },\n {\n key: 'loadLimitInternal',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Display...',\n addonRight: {\n text: 'results per page'\n },\n max: 500,\n required: true,\n help: 'Determines the number of results shown on one page. This might also cause more API hits because indexers are queried until the number of results is matched or all indexers are exhausted. Limit is 500.',\n advanced: true\n }\n },\n {\n key: 'coverSize',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Cover width',\n addonRight: {\n text: 'px'\n },\n required: true,\n help: 'Determines width of covers in search results (when enabled in display options).'\n }\n }\n ]\n }, {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Quick filters'\n },\n fieldGroup: [\n {\n key: 'showQuickFilterButtons',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show quick filters',\n help: 'Show quick filter buttons for movie and TV results.'\n }\n },\n {\n key: 'alwaysShowQuickFilterButtons',\n type: 'horizontalSwitch',\n hideExpression: '!model.showQuickFilterButtons',\n templateOptions: {\n type: 'switch',\n label: 'Always show quick filters',\n help: 'Show all quick filter buttons for all types of searches.',\n advanced: true\n }\n },\n {\n key: 'customQuickFilterButtons',\n type: 'horizontalChips',\n hideExpression: '!model.showQuickFilterButtons',\n templateOptions: {\n type: 'text',\n label: 'Custom quick filters',\n help: 'Enter in the format \"DisplayName=Required1,Required2\". Apply values with enter key.',\n tooltip: 'E.g. use \"WEB=webdl,web-dl.\" for a quick filter with the name \"WEB\" to be displayed that searches for \"webdl\" and \"web-dl\" in lowercase search results.',\n advanced: true\n }\n\n },\n {\n key: 'preselectQuickFilterButtons',\n type: 'horizontalMultiselect',\n hideExpression: '!model.showQuickFilterButtons',\n templateOptions: {\n label: 'Preselect quickfilters',\n help: 'Choose which quickfilters will be selected by default.',\n options: [\n {id: 'source|camts', label: 'CAM / TS'},\n {id: 'source|tv', label: 'TV'},\n {id: 'source|web', label: 'WEB'},\n {id: 'source|dvd', label: 'DVD'},\n {id: 'source|bluray', label: 'Blu-Ray'},\n {id: 'quality|q480p', label: '480p'},\n {id: 'quality|q720p', label: '720p'},\n {id: 'quality|q1080p', label: '1080p'},\n {id: 'quality|q2160p', label: '2160p'},\n {id: 'other|q3d', label: '3D'},\n {id: 'other|qx265', label: 'x265'},\n {id: 'other|qhevc', label: 'HEVC'},\n ],\n optionsFunction: function (model) {\n var customQuickFilters = [];\n _.each(model.customQuickFilterButtons, function (entry) {\n var split1 = entry.split(\"=\");\n var displayName = split1[0];\n customQuickFilters.push({id: \"custom|\" + displayName, label: displayName})\n })\n return customQuickFilters;\n },\n tooltip: 'To select custom quickfilters you just entered please save the config first.',\n buttonText: \"None\",\n advanced: true\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Duplicate detection',\n tooltip: 'Hydra tries to find duplicate results from different indexers using heuristics. You can control the parameters for that but usually the default values work quite well.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'duplicateSizeThresholdInPercent',\n type: 'horizontalPercentInput',\n templateOptions: {\n type: 'text',\n label: 'Duplicate size threshold',\n required: true,\n addonRight: {\n text: '%'\n }\n\n }\n },\n {\n key: 'duplicateAgeThreshold',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Duplicate age threshold',\n required: true,\n addonRight: {\n text: 'hours'\n }\n }\n }\n\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Other',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'keepSearchResultsForDays',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Store results for ...',\n addonRight: {\n text: 'days'\n },\n required: true,\n tooltip: 'Found results are stored in the database for this long until they\\'re deleted. After that any links to Hydra results still stored elsewhere become invalid. You can increase the limit if you want, the disc space needed is negligible (about 75 MB for 7 days on my server).'\n }\n },\n {\n key: 'globalCacheTimeMinutes',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Results cache time',\n help: 'When set search results will be cached for this time. Any search with the same parameters will return the cached results. API cache time parameters will be preferred. See wiki.',\n addonRight: {\n text: 'minutes'\n }\n }\n }\n ]\n }\n ],\n\n categoriesConfig: [\n {\n key: 'enableCategorySizes',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Category sizes',\n help: \"Preset min and max sizes depending on the selected category\",\n tooltip: 'Preset range of minimum and maximum sizes for its categories. When you select a category in the search area the appropriate fields are filled with these values.'\n }\n },\n {\n key: 'defaultCategory',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Default category',\n options: [],\n help: \"Set a default category. Reload page to set a category you just added.\"\n },\n controller: function ($scope) {\n var options = [];\n options.push({name: 'All', value: 'All'});\n _.each($scope.model.categories, function (cat) {\n options.push({name: cat.name, value: cat.name});\n });\n $scope.to.options = options;\n }\n },\n {\n type: 'help',\n templateOptions: {\n type: 'help',\n lines: [\n \"The category configuration is not validated in any way. You can seriously fuck up Hydra's results and overall behavior so take care.\",\n \"Restrictions will taken from a result's category, not the search request category which may not always be the same.\"\n ],\n marginTop: '50px',\n advanced: true\n }\n },\n {\n type: 'repeatSection',\n key: 'categories',\n model: rootModel.categoriesConfig,\n templateOptions: {\n btnText: 'Add new category',\n headline: 'Categories',\n advanced: true,\n fields: [\n {\n key: 'name',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Name',\n help: 'Renaming categories might cause problems with repeating searches from the history.',\n required: true\n }\n },\n {\n key: 'searchType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Search type',\n options: [\n {name: 'General', value: 'SEARCH'},\n {name: 'Audio', value: 'MUSIC'},\n {name: 'EBook', value: 'BOOK'},\n {name: 'Movie', value: 'MOVIE'},\n {name: 'TV', value: 'TVSEARCH'}\n ],\n help: \"Determines how indexers will be searched and if autocompletion is available in the GUI\"\n }\n },\n {\n key: 'subtype',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Sub type',\n options: [\n {name: 'Anime', value: 'ANIME'},\n {name: 'Audiobook', value: 'AUDIOBOOK'},\n {name: 'Comic', value: 'COMIC'},\n {name: 'Ebook', value: 'EBOOK'},\n {name: 'None', value: 'NONE'}\n ],\n help: \"Special search type. Used for indexer specific mappings between categories and newznab IDs\"\n }\n },\n {\n key: 'applyRestrictionsType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Apply restrictions',\n options: [\n {name: 'All searches', value: 'BOTH'},\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"For which type of search word restrictions will be applied\"\n }\n },\n {\n key: 'requiredWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Required words',\n help: \"Must *all* be present in a title which is converted to lowercase before. Apply words with return key.\"\n }\n },\n {\n key: 'requiredRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Required regex',\n help: 'Must be present in a title (case is ignored).'\n }\n },\n {\n key: 'forbiddenWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden words',\n help: \"None may be present in a title which is converted to lowercase before. Apply words with return key.\"\n }\n },\n {\n key: 'forbiddenRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Forbidden regex',\n help: 'Must not be present in a title (case is ignored).'\n }\n },\n {\n wrapper: 'settingWrapper',\n templateOptions: {\n label: 'Size preset',\n help: \"Will set these values on the search page\"\n },\n fieldGroup: [\n {\n key: 'minSizePreset',\n type: 'duoSetting',\n templateOptions: {\n addonRight: {\n text: 'MB'\n }\n\n }\n },\n {\n type: 'duolabel'\n },\n {\n key: 'maxSizePreset',\n type: 'duoSetting', templateOptions: {addonRight: {text: 'MB'}}\n }\n ]\n },\n {\n key: 'applySizeLimitsToApi',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Limit API results size',\n help: \"Enable to apply the size preset to API results from this category\"\n }\n },\n {\n key: 'newznabCategories',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Newznab categories',\n help: 'Map newznab categories to Hydra categories. Used for parsing and when searching internally. Apply categories with return key.',\n tooltip: 'Hydra tries to map API search (newnzab) categories to its internal list of categories, going from specific to general. Example: If an API search is done with a catagory that matches those of \"Movies HD\" the settings for that category are used. Otherwise it checks if it matches the \"Movies\" category and, if yes, uses that one. If that one doesn\\'t match no category settings are used.

' +\n 'Related to that you must also define the newznab categories for every Hydra category, e.g. decide if the category for foreign movies (2010) is used for movie searches. This also controls the category mapping described above. You may combine newznab categories using \"&\" to require multiple numbers to be present in a result. For example \"2010&11000\" would require a search result to contain both 2010 and 11000 for that category to match.

' +\n 'Note: When an API search defines categories the internal mapping is only used for the forbidden and required words. The search requests to your newznab indexers will still use the categories from the original request, not the ones configured here.'\n }\n },\n {\n key: 'ignoreResultsFrom',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Ignore results',\n options: [\n {name: 'For all searches', value: 'BOTH'},\n {name: 'For internal searches', value: 'INTERNAL'},\n {name: 'For API searches', value: 'API'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"Ignore results from this category\",\n tooltip: 'If you want you can entirely ignore results from categories. Results from these categories will not show in the searches. If you select \"Internal\" or \"Always\" this category will also not be selectable on the search page.'\n }\n }\n\n ],\n defaultModel: {\n name: null,\n applySizeLimitsToApi: false,\n applyRestrictionsType: \"NONE\",\n forbiddenRegex: null,\n forbiddenWords: [],\n ignoreResultsFrom: \"NONE\",\n mayBeSelected: true,\n maxSizePreset: null,\n minSizePreset: null,\n newznabCategories: [],\n preselect: true,\n requiredRegex: null,\n requiredWords: [],\n searchType: \"SEARCH\",\n subtype: \"NONE\"\n }\n }\n }\n ],\n downloading: [\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'General',\n tooltip: 'Hydra allows sending NZB search results directly to downloaders (NZBGet, sabnzbd). Torrent downloaders are not supported.'\n },\n fieldGroup: [\n {\n key: 'saveTorrentsTo',\n type: 'fileInput',\n templateOptions: {\n label: 'Torrent black hole',\n help: 'Allow torrents to be saved in this folder from the search results. Ignored if not set.',\n type: \"folder\"\n }\n },\n {\n key: 'saveNzbsTo',\n type: 'fileInput',\n templateOptions: {\n label: 'NZB black hole',\n help: 'Allow NZBs to be saved in this folder from the search results. Ignored if not set.',\n type: \"folder\"\n }\n },\n {\n key: 'nzbAccessType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'NZB access type',\n options: [\n {name: 'Proxy NZBs from indexer', value: 'PROXY'},\n {name: 'Redirect to the indexer', value: 'REDIRECT'}\n ],\n help: \"How access to NZBs is provided when NZBs are downloaded (by the user or external tools). Proxying is recommended as it allows fallback for failed downloads (see below)..\",\n tooltip: 'NZB downloads from Hydra can either be achieved by redirecting the requester to the original indexer or by downloading the NZB from the indexer and serving this. Redirecting has the advantage that it causes the least load on Hydra but also the disadvantage that the requester might be forwarded to an indexer link that contains the indexer\\'s API key. To prevent that select to proxy NZBs. It also allows fallback for failed downloads (next option).',\n advanced: true\n\n }\n },\n {\n key: 'externalUrl',\n type: 'horizontalInput',\n hideExpression: function ($viewValue, $modelValue, scope) {\n return !_.any(scope.model.downloaders, function (downloader) {\n return downloader.nzbAddingType === \"SEND_LINK\";\n });\n },\n templateOptions: {\n label: 'External URL',\n help: 'Used for links when sending links to the downloader.',\n tooltip: 'When using \"Add links\" to add NZBs to your downloader the links are usually calculated using the URL with which you accessed NZBHydra. This might be a URL that\\'s not accessible by the downloader (e.g. when it\\'s inside a docker container). Set the URL for NZBHydra that\\'s accessible by the downloader here and it will be used instead. ',\n advanced: true\n }\n },\n\n {\n key: 'fallbackForFailed',\n type: 'horizontalSelect',\n hideExpression: 'model.nzbAccessType === \"REDIRECT\"',\n templateOptions: {\n label: 'Fallback for failed downloads',\n options: [\n {name: 'GUI downloads', value: 'INTERNAL'},\n {name: 'API downloads', value: 'API'},\n {name: 'All downloads', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"Fallback to similar results when a download fails. Only available when proxying NZBs (see above).\",\n tooltip: \"When you or an external program tries to download an NZB from NZBHydra the download may fail because the indexer is offline or its download limit has been reached. You can use this setting for NZBHydra to try and fall back on results from other indexers. It will search for results with the same name that were the result from the same search as where the download originated from. It will *not* execute another search.\"\n }\n },\n {\n key: 'sendMagnetLinks',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Send magnet links',\n help: \"Enable to send magnet links to the associated program on the server machine. Won't work with docker\"\n }\n },\n {\n key: 'updateStatuses',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Update statuses',\n help: \"Query your downloader for status updates of downloads\",\n advanced: true\n }\n },\n {\n key: 'showDownloaderStatus',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show downloader footer',\n help: \"Show footer with downloader status\",\n advanced: true\n }\n },\n {\n key: 'primaryDownloader',\n type: 'horizontalSelect',\n hideExpression: 'model.downloaders.length <= 1 || !model.showDownloaderStatus',\n templateOptions: {\n label: 'Primary downloader',\n options: [],\n help: \"This downloader's state will be shown in the footer.\",\n tooltip: \"To select a downloader you just added please save the config first.\",\n optionsFunction: function (model) {\n var downloaders = [];\n _.each(model.downloaders, function (downloader) {\n downloaders.push({name: downloader.name, value: downloader.name})\n })\n return downloaders;\n },\n optionsFunctionAfter: function (model) {\n if (!model.primaryDownloader) {\n model.primaryDownloader = model.downloaders[0].name;\n }\n }\n }\n },\n ]\n },\n {\n wrapper: 'fieldset',\n key: 'downloaders',\n templateOptions: {label: 'Downloaders'},\n fieldGroup: [\n {\n type: \"downloaderConfig\",\n data: {}\n }\n ]\n }\n ],\n\n indexers: [\n {\n type: \"indexers\",\n data: {}\n },\n {\n type: 'recheckAllCaps'\n }\n ],\n auth: [\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Main',\n\n },\n fieldGroup: [\n {\n key: 'authType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Auth type',\n options: [\n {name: 'None', value: 'NONE'},\n {name: 'HTTP Basic auth', value: 'BASIC'},\n {name: 'Login form', value: 'FORM'}\n ],\n tooltip: '
    ' +\n '
  • With auth type \"None\" all areas are unrestricted.
  • ' +\n '
  • With auth type \"Form\" the basic page is loaded and login is done via a form.
  • ' +\n '
  • With auth type \"Basic\" you login via basic HTTP authentication. With all areas restricted this is the most secure as nearly no data is loaded from the server before you auth. Logging out is not supported with basic auth.
  • ' +\n '
'\n }\n },\n {\n key: 'authHeader',\n type: 'horizontalInput',\n templateOptions: {\n type: 'string',\n label: 'Auth header',\n help: 'Name of header that provides the username in requests from secure sources.',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n }\n },\n {\n key: 'authHeaderIpRanges',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Secure IP ranges',\n help: 'IP ranges from which the auth header will be accepted. Apply with return key. Use values like \"192.168.0.1-192.168.0.100\" or single IP addresses like \"127.0.0.1\".',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\" || _.isNullOrEmpty(rootModel.auth.authHeader);\n }\n },\n {\n key: 'rememberUsers',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Remember users',\n help: 'Remember users with cookie for 14 days.'\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n }\n },\n {\n key: 'rememberMeValidityDays',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Cookie expiry',\n help: 'How long users are remembered.',\n addonRight: {\n text: 'days'\n },\n advanced: true\n }\n }\n\n ]\n },\n\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Restrictions',\n tooltip: 'Select which areas/features can only be accessed by logged in users (i.e. are restricted). If you don\\'t to allow anonymous users to do anything just leave everything selected.
You can decide for every user if he is allowed to:
' +\n '
    \\n' +\n '
  • view the search page at all
  • \\n' +\n '
  • view the stats
  • \\n' +\n '
  • access the admin area (config and control)
  • \\n' +\n '
  • view links for downloading NZBs and see their details
  • \\n' +\n '
  • may select which indexers are used for search.
  • \\n' +\n '
'\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n },\n fieldGroup: [\n {\n key: 'restrictSearch',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict searching',\n help: 'Restrict access to searching.'\n }\n },\n {\n key: 'restrictStats',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict stats',\n help: 'Restrict access to stats.'\n }\n },\n {\n key: 'restrictAdmin',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict admin',\n help: 'Restrict access to admin functions.'\n }\n },\n {\n key: 'restrictDetailsDl',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict NZB details & DL',\n help: 'Restrict NZB details, comments and download links.'\n }\n },\n {\n key: 'restrictIndexerSelection',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict indexer selection box',\n help: 'Restrict visibility of indexer selection box in search. Affects only GUI.'\n }\n },\n {\n key: 'allowApiStats',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Allow stats access',\n help: 'Allow access to stats via external API.'\n }\n }\n ]\n },\n\n {\n type: 'repeatSection',\n key: 'users',\n model: rootModel.auth,\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n },\n templateOptions: {\n btnText: 'Add new user',\n altLegendText: 'Authless',\n headline: 'Users',\n fields: [\n {\n key: 'username',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Username',\n required: true\n }\n },\n {\n key: 'password',\n type: 'passwordSwitch',\n templateOptions: {\n type: 'password',\n label: 'Password',\n required: true\n }\n },\n {\n key: 'maySeeAdmin',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see admin area'\n }\n },\n {\n key: 'maySeeStats',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see stats'\n },\n hideExpression: 'model.maySeeAdmin'\n },\n {\n key: 'maySeeDetailsDl',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see NZB details & DL links'\n },\n hideExpression: 'model.maySeeAdmin'\n },\n {\n key: 'showIndexerSelection',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see indexer selection box'\n },\n hideExpression: 'model.maySeeAdmin'\n }\n ],\n defaultModel: {\n username: null,\n password: null,\n token: null,\n maySeeStats: true,\n maySeeAdmin: true,\n maySeeDetailsDl: true,\n showIndexerSelection: true\n }\n }\n }\n ],\n notificationConfig: [\n {\n type: 'help',\n templateOptions: {\n type: 'help',\n lines: [\n \"NZBHydra supports sending and displaying notifications for certain events. You can enable notifications for each event by adding entries below.\",\n 'NZBHydra uses Apprise to communicate with the actual notification providers. You need either a) an instance of Apprise API running or b) an Apprise runnable accessible by NZBHydra. Either are not part of NZBHydra.',\n \"NZBHydra will also show notifications on the GUI if enabled.\"\n ]\n }\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Main'\n },\n fieldGroup: [\n\n {\n key: 'appriseType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Apprise type',\n options: [\n {name: 'None', value: 'NONE'},\n {name: 'API', value: 'API'},\n {name: 'CLI', value: 'CLI'}\n ]\n }\n },\n {\n key: 'appriseApiUrl',\n type: 'horizontalInput',\n templateOptions: {\n type: 'string',\n label: 'Apprise API URL',\n help: 'URL of Apprise API to send notifications to.'\n },\n hideExpression: 'model.appriseType !== \"API\"'\n },\n {\n key: 'appriseCliPath',\n type: 'fileInput',\n templateOptions: {\n type: 'file',\n label: 'Apprise runnable',\n help: 'Full path of of Apprise runnable to execute.'\n },\n hideExpression: 'model.appriseType !== \"CLI\"'\n },\n {\n key: 'displayNotifications',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Display notifications',\n help: 'If enabled notifications will be shown on the GUI.'\n }\n },\n {\n key: 'displayNotificationsMax',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Show max notifications',\n help: 'Max number of notifications to show on the GUI. If more have piled up a notification will indicate this and link to the notification history.'\n },\n hideExpression: '!model.displayNotifications'\n },\n {\n key: 'filterOuts',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Hide if message contains...',\n help: 'Apply values with return key. Surround with \"/\" for regex (e.g. /contains[0-9]This/). Case insensitive.',\n\n },\n hideExpression: '!model.displayNotifications'\n }\n ]\n },\n\n {\n type: 'notificationSection',\n key: 'entries',\n model: rootModel.notificationConfig,\n templateOptions: {\n btnText: 'Add new notification',\n altLegendText: 'Notification',\n headline: 'Notifications',\n fields: [\n {\n key: 'appriseUrls',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'URLs',\n help: 'One or more URLs identifying where the notification should be sent to, comma-separated.'\n }\n },\n {\n key: 'titleTemplate',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Title template'\n },\n controller: notificationTemplateHelpController\n },\n {\n key: 'bodyTemplate',\n type: 'horizontalTextArea',\n templateOptions: {\n type: 'text',\n label: 'Body template',\n required: true\n },\n controller: notificationTemplateHelpController\n },\n {\n key: 'messageType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Message type',\n options: [\n {name: 'Info', value: 'INFO'},\n {name: 'Success', value: 'SUCCESS'},\n {name: 'Warning', value: 'WARNING'},\n {name: 'Failure', value: 'FAILURE'}\n ],\n help: \"Select the message type to use.\"\n }\n },\n {\n key: 'bodyTemplate',\n type: 'horizontalTestNotification'\n }\n\n ],\n defaultModel: {\n eventType: null,\n appriseUrls: null,\n titleTemplate: null,\n bodyTemplate: null,\n messageType: 'WARNING'\n }\n }\n }\n ]\n\n }\n\n function notificationTemplateHelpController($scope, NotificationService) {\n $scope.model.eventTypeReadable = NotificationService.humanize($scope.model.eventType);\n $scope.to.help = NotificationService.getTemplateHelp($scope.model.eventType);\n }\n }\n}\n\nfunction handleConnectionCheckFail(ModalService, data, model, whatFailed, deferred) {\n var message;\n var yesText;\n if (data.checked) {\n message = \"The connection to the \" + whatFailed + \" failed: \" + data.message + \"
Do you want to add it anyway?\";\n yesText = \"I know what I'm doing\";\n } else {\n message = \"The connection to the \" + whatFailed + \" could not be tested, sorry. Please check the log.\";\n yesText = \"I'll risk it\";\n }\n ModalService.open(\"Connection check failed\", message, {\n yes: {\n onYes: function () {\n deferred.resolve();\n },\n text: yesText\n },\n no: {\n onNo: function () {\n model.enabled = false;\n deferred.resolve();\n },\n text: \"Add it, but disabled\"\n },\n cancel: {\n onCancel: function () {\n deferred.reject();\n },\n text: \"Aahh, let me try again\"\n }\n });\n}\n","\nConfigController.$inject = [\"$scope\", \"$http\", \"activeTab\", \"ConfigService\", \"config\", \"DownloaderCategoriesService\", \"ConfigFields\", \"ConfigModel\", \"ModalService\", \"RestartService\", \"localStorageService\", \"$state\", \"growl\", \"$window\"];angular\n .module('nzbhydraApp')\n .factory('ConfigModel', function () {\n return {};\n });\n\nangular\n .module('nzbhydraApp')\n .factory('ConfigWatcher', function () {\n var $scope;\n\n return {\n watch: watch\n };\n\n function watch(scope) {\n $scope = scope;\n $scope.$watchGroup([\"config.main.host\"], function () {\n }, true);\n }\n });\n\n\nangular\n .module('nzbhydraApp')\n .controller('ConfigController', ConfigController);\n\nfunction ConfigController($scope, $http, activeTab, ConfigService, config, DownloaderCategoriesService, ConfigFields, ConfigModel, ModalService, RestartService, localStorageService, $state, growl, $window) {\n $scope.config = config;\n $scope.submit = submit;\n $scope.activeTab = activeTab;\n\n $scope.restartRequired = false;\n $scope.ignoreSaveNeeded = false;\n console.log(localStorageService.get(\"showAdvanced\"));\n if (localStorageService.get(\"showAdvanced\") === null) {\n $scope.showAdvanced = false;\n localStorageService.set(\"showAdvanced\", false);\n } else {\n $scope.showAdvanced = localStorageService.get(\"showAdvanced\");\n }\n\n\n $scope.toggleShowAdvanced = function () {\n $scope.showAdvanced = !$scope.showAdvanced;\n var wasDirty = $scope.form.$dirty === true;\n\n $scope.allTabs[$scope.activeTab].model.showAdvanced = $scope.showAdvanced === true;\n //Also save in main tab where it will be stored to file\n $scope.allTabs[0].model.showAdvanced = $scope.allTabs[$scope.activeTab].model.showAdvanced === true;\n $scope.form.$dirty = wasDirty;\n localStorageService.set(\"showAdvanced\", $scope.showAdvanced);\n }\n\n function updateAndAskForRestartIfNecessary(responseData) {\n if (angular.isUndefined($scope.form)) {\n console.error(\"Unable to determine if a restart is necessary\");\n return;\n }\n\n $scope.form.$setPristine();\n DownloaderCategoriesService.invalidate();\n if ($scope.restartRequired) {\n ModalService.open(\"Restart required\", \"The changes you have made may require a restart to be effective.
Do you want to restart now?\", {\n yes: {\n onYes: function () {\n RestartService.restart();\n }\n },\n no: {\n onNo: function ($uibModalInstance) {\n //Needs to be clicked twice for some reason\n $scope.restartRequired = false;\n $uibModalInstance.dismiss();\n $uibModalInstance.dismiss();\n $scope.config = responseData.newConfig;\n $window.location.reload();\n }\n }\n });\n } else {\n $scope.config = responseData.newConfig;\n $window.location.reload();\n }\n }\n\n function handleConfigSetResponse(response, ignoreWarnings, restartNeeded) {\n if (angular.isUndefined(ignoreWarnings)) {\n ignoreWarnings = localStorageService.get(\"ignoreWarnings\") !== null ? localStorageService.get(\"ignoreWarnings\") : false;\n }\n //Communication with server was successful but there might be validation errors and/or warnings\n var warningMessages = response.data.warningMessages;\n var errorMessages = response.data.errorMessages;\n $scope.restartRequired = response.data.restartNeeded || (angular.isDefined(restartNeeded) ? restartNeeded : false);\n var showMessage = errorMessages.length > 0 || (warningMessages.length > 0 && !ignoreWarnings);\n\n function extendMessageWithList(message, messages) {\n _.forEach(messages, function (x) {\n message += \"
  • \" + x + \"
  • \";\n });\n message += \"\";\n return message;\n }\n\n if (showMessage) {\n var options;\n var message;\n var title;\n if (errorMessages.length > 0) { //Actual errors which cannot be ignored\n title = \"Config validation failed\";\n message = 'The following errors have been found in your config. They need to be fixed.
      ';\n message = extendMessageWithList(message, response.data.errorMessages);\n if (warningMessages.length > 0) {\n message += '
      The following warnings were found. You can ignore them if you wish.
        ';\n message = extendMessageWithList(message, response.data.warningMessages);\n }\n options = {\n yes: {\n onYes: function () {\n },\n text: \"OK\"\n }\n };\n } else if (warningMessages.length > 0) {\n title = \"Config validation warnings\";\n message = '
        The following warnings have been found. You can ignore them if you wish. The config was already saved.
          ';\n message = extendMessageWithList(message, response.data.warningMessages);\n options = {\n // cancel: {\n // onCancel: function () {\n // $scope.form.$setPristine();\n // localStorageService.set(\"ignoreWarnings\", true);\n // ConfigService.set($scope.config, true).then(function (response) {\n // handleConfigSetResponse(response, true, $scope.restartRequired);\n // updateAndAskForRestartIfNecessary(response.data);\n // }, function (response) {\n // //Actual error while setting or validating config\n // growl.error(response.data);\n // });\n // },\n // text: \"OK, don't show warnings again\"\n // },\n yes: {\n onYes: function () {\n handleConfigSetResponse(response, true, $scope.restartRequired);\n updateAndAskForRestartIfNecessary(response.data);\n },\n text: \"OK\"\n }\n };\n }\n ModalService.open(title, message, options, \"md\", \"left\");\n } else {\n updateAndAskForRestartIfNecessary(response.data);\n }\n }\n\n function submit() {\n if ($scope.form.$valid && !$scope.myShowError) {\n ConfigService.set($scope.config, true).then(function (response) {\n handleConfigSetResponse(response);\n }, function (response) {\n //Actual error while setting or validating config\n growl.error(response.data);\n });\n\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n\n //Ridiculously hacky way to make the error messages appear\n try {\n if (angular.isDefined(form.$error.required)) {\n _.each(form.$error.required, function (item) {\n if (angular.isDefined(item.$error.required)) {\n _.each(item.$error.required, function (item2) {\n item2.$setTouched();\n });\n }\n });\n }\n angular.forEach($scope.form.$error.required, function (field) {\n field.$setTouched();\n });\n } catch (err) {\n //\n }\n\n }\n }\n\n ConfigModel = config;\n\n $scope.fields = ConfigFields.getFields($scope.config);\n\n $scope.allTabs = [\n {\n active: false,\n state: 'root.config.main',\n name: 'Main',\n model: ConfigModel.main,\n fields: $scope.fields.main\n },\n {\n active: false,\n state: 'root.config.auth',\n name: 'Authorization',\n model: ConfigModel.auth,\n fields: $scope.fields.auth,\n options: {}\n },\n {\n active: false,\n state: 'root.config.searching',\n name: 'Searching',\n model: ConfigModel.searching,\n fields: $scope.fields.searching,\n options: {}\n },\n {\n active: false,\n state: 'root.config.categories',\n name: 'Categories',\n model: ConfigModel.categoriesConfig,\n fields: $scope.fields.categoriesConfig,\n options: {}\n },\n {\n active: false,\n state: 'root.config.downloading',\n name: 'Downloading',\n model: ConfigModel.downloading,\n fields: $scope.fields.downloading,\n options: {}\n },\n {\n active: false,\n state: 'root.config.indexers',\n name: 'Indexers',\n model: ConfigModel.indexers,\n fields: $scope.fields.indexers,\n options: {}\n },\n {\n active: false,\n state: 'root.config.notifications',\n name: 'Notifications',\n model: ConfigModel.notificationConfig,\n fields: $scope.fields.notificationConfig,\n options: {}\n }\n ];\n\n //Copy showAdvanced setting over from main tab's setting\n _.each($scope.allTabs, function (tab) {\n tab.model.showAdvanced = $scope.showAdvanced === true;\n })\n\n $scope.isSavingNeeded = function () {\n return $scope.form.$dirty && $scope.form.$valid && !$scope.ignoreSaveNeeded;\n };\n\n $scope.goToConfigState = function (index) {\n $state.go($scope.allTabs[index].state, {activeTab: index}, {inherit: false, notify: true, reload: true});\n };\n\n $scope.apiHelp = function () {\n\n if ($scope.isSavingNeeded()) {\n growl.info(\"Please save first\");\n return;\n }\n var apiHelp = ConfigService.apiHelp().then(function (data) {\n\n var html = '' +\n '' +\n '' +\n '' +\n '
          Newznab API endpoint:%newznab%
          Torznab API endpoint:%torznab%
          API key:%apikey%
          ';\n //Torznab API endpoint: %torznab%
          API key: %apikey%\n html = html.replace(\"%newznab%\", data.newznabApi);\n html = html.replace(\"%torznab%\", data.torznabApi);\n html = html.replace(\"%apikey%\", data.apiKey);\n ModalService.open(\"API infos\", html, {}, \"md\");\n });\n };\n\n $scope.configureIn = function (externalTool) {\n\n if ($scope.isSavingNeeded()) {\n growl.info(\"Please save first\");\n return;\n }\n ConfigService.configureIn(externalTool);\n };\n\n $scope.$on('$stateChangeStart',\n function (event, toState, toParams, fromState, fromParams) {\n if ($scope.isSavingNeeded()) {\n event.preventDefault();\n ModalService.open(\"Unsaved changed\", \"Do you want to save before leaving?\", {\n yes: {\n onYes: function () {\n $scope.submit();\n $state.go(toState);\n },\n text: \"Yes\"\n },\n no: {\n onNo: function () {\n $scope.ignoreSaveNeeded = true;\n $scope.allTabs[$scope.activeTab].options.resetModel();\n $state.go(toState);\n },\n text: \"No\"\n },\n cancel: {\n onCancel: function () {\n event.preventDefault();\n },\n text: \"Cancel\"\n }\n });\n }\n });\n\n $scope.$watch(\"$scope.form.$valid\", function () {\n });\n\n $scope.$on('$formValidity', function (event, isValid) {\n console.log(\"Received $formValidity event: \" + isValid);\n $scope.form.$valid = isValid;\n $scope.form.$invalid = !isValid;\n $scope.showError = !isValid;\n $scope.myShowError = !isValid;\n });\n}\n\n\n","\nUpdateService.$inject = [\"$http\", \"growl\", \"blockUI\", \"RestartService\", \"RequestsErrorHandler\", \"$uibModal\", \"$timeout\"];\nUpdateModalInstanceCtrl.$inject = [\"$scope\", \"$http\", \"$interval\", \"RequestsErrorHandler\"];angular\n .module('nzbhydraApp')\n .factory('UpdateService', UpdateService);\n\nfunction UpdateService($http, growl, blockUI, RestartService, RequestsErrorHandler, $uibModal, $timeout) {\n\n var currentVersion;\n var latestVersion;\n var betaVersion;\n var updateAvailable;\n var betaUpdateAvailable;\n var latestVersionIgnored;\n var betaVersionsEnabled;\n var versionHistory;\n var updatedExternally;\n var automaticUpdateToNotice;\n\n\n return {\n update: update,\n showChanges: showChanges,\n getInfos: getInfos,\n getVersionHistory: getVersionHistory,\n ignore: ignore,\n showChangesFromAutomaticUpdate: showChangesFromAutomaticUpdate\n };\n\n function getInfos() {\n return RequestsErrorHandler.specificallyHandled(function () {\n return $http.get(\"internalapi/updates/infos\").then(\n function (response) {\n currentVersion = response.data.currentVersion;\n latestVersion = response.data.latestVersion;\n betaVersion = response.data.betaVersion;\n updateAvailable = response.data.updateAvailable;\n betaUpdateAvailable = response.data.betaUpdateAvailable;\n latestVersionIgnored = response.data.latestVersionIgnored;\n betaVersionsEnabled = response.data.betaVersionsEnabled;\n updatedExternally = response.data.updatedExternally;\n automaticUpdateToNotice = response.data.automaticUpdateToNotice;\n return response;\n }, function () {\n\n }\n );\n });\n }\n\n function ignore(version) {\n return $http.put(\"internalapi/updates/ignore/\" + version).then(function (response) {\n return response;\n });\n }\n\n function getVersionHistory() {\n return $http.get(\"internalapi/updates/versionHistory\").then(function (response) {\n versionHistory = response.data;\n return response;\n });\n }\n\n function showChanges(version) {\n return $http.get(\"internalapi/updates/changesSince/\" + version).then(function (response) {\n var params = {\n size: \"lg\",\n templateUrl: \"static/html/changelog-modal.html\",\n resolve: {\n versionHistory: function () {\n return response.data;\n }\n },\n controller: function ($scope, $sce, $uibModalInstance, versionHistory) {\n $scope.versionHistory = versionHistory;\n\n $scope.ok = function () {\n $uibModalInstance.dismiss();\n };\n }\n };\n\n var modalInstance = $uibModal.open(params);\n modalInstance.result.then();\n });\n }\n\n function showChangesFromAutomaticUpdate() {\n return $http.get(\"internalapi/updates/automaticUpdateVersionHistory\").then(function (response) {\n var params = {\n size: \"lg\",\n templateUrl: \"static/html/changelog-modal.html\",\n resolve: {\n versionHistory: function () {\n return response.data;\n }\n },\n controller: function ($scope, $sce, $uibModalInstance, versionHistory) {\n $scope.versionHistory = versionHistory;\n\n $scope.ok = function () {\n $uibModalInstance.dismiss();\n };\n }\n };\n\n var modalInstance = $uibModal.open(params);\n modalInstance.result.then();\n return $http.get(\"internalapi/updates/ackAutomaticUpdateVersionHistory\").then(function (response) {\n\n });\n });\n }\n\n\n function update(version) {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/update-modal.html',\n controller: 'UpdateModalInstanceCtrl',\n size: \"md\",\n backdrop: 'static',\n keyboard: false\n });\n $http.put(\"internalapi/updates/installUpdate/\" + version).then(function () {\n //Handle like restart, ping application and wait\n //Perhaps save the version to which we want to update, ask later and see if they're equal. If not updating apparently failed...\n $timeout(function () {\n //Give user some time to read the last message\n RestartService.startCountdown(\"\");\n modalInstance.close();\n }, 2000);\n },\n function () {\n growl.info(\"An error occurred while updating. Please check the logs.\");\n modalInstance.close();\n });\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('UpdateModalInstanceCtrl', UpdateModalInstanceCtrl);\n\nfunction UpdateModalInstanceCtrl($scope, $http, $interval, RequestsErrorHandler) {\n $scope.messages = [];\n\n var interval = $interval(function () {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get(\"internalapi/updates/messages\").then(\n function (data) {\n $scope.messages = data.data;\n }\n );\n });\n },\n 200);\n\n $scope.$on('$destroy', function () {\n if (interval !== null) {\n $interval.cancel(interval);\n }\n });\n\n}\n","\nSystemController.$inject = [\"$scope\", \"$state\", \"activeTab\", \"simpleInfos\", \"$http\", \"growl\", \"RestartService\", \"MigrationService\", \"ConfigService\", \"NzbHydraControlService\", \"RequestsErrorHandler\"];angular\n .module('nzbhydraApp')\n .controller('SystemController', SystemController);\n\nfunction SystemController($scope, $state, activeTab, simpleInfos, $http, growl, RestartService, MigrationService, ConfigService, NzbHydraControlService, RequestsErrorHandler) {\n\n $scope.activeTab = activeTab;\n $scope.foo = {\n csv: \"\",\n sql: \"\"\n };\n\n $scope.simpleInfos = simpleInfos;\n\n $scope.shutdown = function () {\n NzbHydraControlService.shutdown().then(function () {\n growl.info(\"Shutdown initiated. Cya!\");\n },\n function () {\n growl.info(\"Unable to send shutdown command.\");\n })\n };\n\n $scope.restart = function () {\n RestartService.restart();\n };\n\n $scope.reloadConfig = function () {\n ConfigService.reloadConfig().then(function () {\n growl.info(\"Successfully reloaded config. Some setting may need a restart to take effect.\")\n }, function (data) {\n growl.error(data.message);\n })\n };\n\n\n $scope.migrate = function () {\n MigrationService.migrate();\n };\n\n\n $scope.allTabs = [\n {\n active: false,\n state: 'root.system.control',\n name: \"Control\"\n },\n {\n active: false,\n state: 'root.system.updates',\n name: \"Updates\"\n },\n {\n active: false,\n state: 'root.system.log',\n name: \"Log\"\n },\n {\n active: false,\n state: 'root.system.tasks',\n name: \"Tasks\"\n },\n {\n active: false,\n state: 'root.system.backup',\n name: \"Backup\"\n },\n {\n active: false,\n state: 'root.system.bugreport',\n name: \"Bugreport / Debug\"\n },\n {\n active: false,\n state: 'root.system.news',\n name: \"News\"\n },\n {\n active: false,\n state: 'root.system.about',\n name: \"About\"\n }\n ];\n\n\n $scope.goToSystemState = function (index) {\n $state.go($scope.allTabs[index].state, {activeTab: index}, {inherit: false, notify: true, reload: true});\n };\n\n $scope.downloadDebuggingInfos = function () {\n $scope.isBackupCreationAction = true;\n $http({\n method: 'GET',\n url: 'internalapi/debuginfos/createAndProvideZipAsBytes',\n responseType: 'arraybuffer'\n }).then(function (response, status, headers, config) {\n var a = document.createElement('a');\n var blob = new Blob([response.data], {'type': \"application/octet-stream\"});\n a.href = URL.createObjectURL(blob);\n a.download = \"nzbhydra-debuginfos-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\n\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n $scope.isBackupCreationAction = false;\n });\n };\n\n $scope.uploadDebuggingInfos = function () {\n $scope.isBackupCreationAction = true;\n $http({\n method: 'GET',\n url: 'internalapi/debuginfos/createAndUploadDebugInfos'\n }).then(function (response) {\n $scope.debugInfosUrl = 'URL with debug infos: ' + response.data + '';\n $scope.isBackupCreationAction = false;\n }, function (response) {\n $scope.debugInfosUrl = response.data;\n $scope.isBackupCreationAction = false;\n });\n };\n\n $scope.logThreadDump = function () {\n $http({\n method: 'GET',\n url: 'internalapi/debuginfos/logThreadDump'\n });\n };\n\n $scope.executeSqlQuery = function () {\n $http.post('internalapi/debuginfos/executesqlquery', $scope.foo.sql).then(function (response) {\n if (response.data.successful) {\n $scope.foo.csv = response.data.message;\n } else {\n growl.error(response.data.message);\n }\n });\n };\n\n $scope.executeSqlUpdate = function () {\n $http.post('internalapi/debuginfos/executesqlupdate', $scope.foo.sql).then(function (response) {\n if (response.data.successful) {\n $scope.foo.csv = response.data.message + \" rows affected\";\n } else {\n growl.error(response.data.message);\n }\n });\n };\n\n\n $scope.cpuChart = {\n options: {\n chart:\n {\n type: 'lineChart',\n height: 450,\n margin: {\n top: 20,\n right: 20,\n bottom: 60,\n left: 65\n },\n x: function (d) {\n return d.time;\n },\n y: function (d) {\n return d.value;\n },\n xAxis: {\n axisLabel: 'Time',\n tickFormat: function (d) {\n return moment.unix(d).local().format(\"HH:mm:ss\");\n },\n showMaxMin: true\n },\n\n yAxis: {\n axisLabel: 'CPU %'\n },\n interactive: true\n }\n },\n data: []\n };\n\n function update() {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get(\"internalapi/debuginfos/threadCpuUsage\", {ignoreLoadingBar: true}).then(function (response) {\n try {\n if (!response) {\n console.error(\"No CPU usage data from server\");\n return;\n }\n $scope.cpuChart.data = response.data;\n\n } catch (e) {\n console.error(e);\n clearInterval(timer);\n }\n },\n function () {\n console.error(\"Error while loading CPU usage data status\");\n clearInterval(timer);\n }\n );\n });\n }\n\n $scope.cpuChart.data = [];\n\n update();\n var timer = setInterval(function () {\n update();\n }, 5000);\n\n $scope.$on('$destroy', function () {\n if (timer !== null) {\n clearInterval(timer);\n }\n });\n\n}\n","\r\nStatsService.$inject = [\"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('StatsService', StatsService);\r\n\r\nfunction StatsService($http) {\r\n\r\n return {\r\n get: getStats,\r\n getDownloadHistory: getDownloadHistory,\r\n getNotificationHistory: getNotificationHistory\r\n };\r\n\r\n function getStats(after, before, includeDisabled, switchState) {\r\n var requestBody = {after: after, before: before, includeDisabled: includeDisabled};\r\n requestBody = _.extend(requestBody, switchState);\r\n return $http.post(\"internalapi/stats\", requestBody).then(function (response) {\r\n return response.data;\r\n });\r\n }\r\n\r\n function buildParams(pageNumber, limit, filterModel, sortModel) {\r\n var params = {page: pageNumber, limit: limit, filterModel: filterModel};\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n } else {\r\n params.sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n }\r\n return params;\r\n }\r\n\r\n function getDownloadHistory(pageNumber, limit, filterModel, sortModel) {\r\n var params = buildParams(pageNumber, limit, filterModel, sortModel);\r\n return $http.post(\"internalapi/history/downloads\", params).then(function (response) {\r\n return {\r\n nzbDownloads: response.data.content,\r\n totalDownloads: response.data.totalElements\r\n };\r\n\r\n });\r\n }\r\n\r\n function getNotificationHistory(pageNumber, limit, filterModel, sortModel) {\r\n var params = buildParams(pageNumber, limit, filterModel, sortModel);\r\n return $http.post(\"internalapi/history/notifications\", params).then(function (response) {\r\n return {\r\n notifications: response.data.content,\r\n totalNotifications: response.data.totalElements\r\n };\r\n\r\n });\r\n }\r\n\r\n}","\r\nStatsController.$inject = [\"$scope\", \"$filter\", \"StatsService\", \"blockUI\", \"localStorageService\", \"$timeout\", \"$window\", \"ConfigService\"];angular\r\n .module('nzbhydraApp')\r\n .controller('StatsController', StatsController);\r\n\r\nfunction StatsController($scope, $filter, StatsService, blockUI, localStorageService, $timeout, $window, ConfigService) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n var initializingAfter = true;\r\n var initializingBefore = true;\r\n $scope.afterDate = moment().subtract(30, \"days\").toDate();\r\n $scope.beforeDate = moment().add(1, \"days\").toDate();\r\n var historyInfoTypeUserEnabled = ConfigService.getSafe().logging.historyUserInfoType === 'USERNAME' || ConfigService.getSafe().logging.historyUserInfoType === 'BOTH';\r\n var historyInfoTypeIpEnabled = ConfigService.getSafe().logging.historyUserInfoType === 'IP' || ConfigService.getSafe().logging.historyUserInfoType === 'BOTH';\r\n $scope.foo = {\r\n includeDisabledIndexersInStats: localStorageService.get(\"includeDisabledIndexersInStats\") !== null ? localStorageService.get(\"includeDisabledIndexersInStats\") : false,\r\n statsSwichState: localStorageService.get(\"statsSwitchState\") !== null ? localStorageService.get(\"statsSwitchState\") :\r\n {\r\n indexerApiAccessStats: true,\r\n avgIndexerUniquenessScore: true,\r\n avgResponseTimes: true,\r\n indexerDownloadShares: true,\r\n downloadsPerDayOfWeek: true,\r\n downloadsPerHourOfDay: true,\r\n searchesPerDayOfWeek: true,\r\n searchesPerHourOfDay: true,\r\n downloadsPerAgeStats: true,\r\n successfulDownloadsPerIndexer: true,\r\n downloadSharesPerUser: historyInfoTypeUserEnabled,\r\n searchSharesPerUser: historyInfoTypeIpEnabled,\r\n downloadSharesPerIp: true,\r\n searchSharesPerIp: true,\r\n userAgentSearchShares: true,\r\n userAgentDownloadShares: true\r\n }\r\n };\r\n localStorageService.set(\"statsSwitchState\", $scope.foo.statsSwichState);\r\n $scope.stats = {};\r\n\r\n updateStats();\r\n\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n $scope.toggleIncludeDisabledIndexers = function () {\r\n localStorageService.set(\"includeDisabledIndexersInStats\", $scope.foo.includeDisabledIndexersInStats);\r\n };\r\n\r\n $scope.onStatsSwitchToggle = function (statId) {\r\n localStorageService.set(\"statsSwitchState\", $scope.foo.statsSwichState);\r\n\r\n if ($scope.foo.statsSwichState[statId]) { //Stat was enabled, get only data for this stat\r\n updateStats(statId);\r\n }\r\n\r\n };\r\n\r\n $scope.refresh = function () {\r\n updateStats();\r\n };\r\n\r\n function updateStats(statId) {\r\n blockUI.start(\"Updating stats...\");\r\n var after = $scope.afterDate !== null ? $scope.afterDate : null;\r\n var before = $scope.beforeDate !== null ? $scope.beforeDate : null;\r\n var statsToRetrieve = {};\r\n if (angular.isDefined(statId)) {\r\n statsToRetrieve[statId] = true;\r\n } else {\r\n statsToRetrieve = $scope.foo.statsSwichState;\r\n }\r\n $scope.statsLoadingPromise = StatsService.get(after, before, $scope.foo.includeDisabledIndexersInStats, statsToRetrieve).then(function (stats) {\r\n $scope.setStats(stats);\r\n //Resize event is needed for the -perUsernameOrIp charts to be properly sized because nvd3 thinks the initial size is 0\r\n $timeout(function () {\r\n $window.dispatchEvent(new Event(\"resize\"));\r\n }, 500);\r\n });\r\n\r\n blockUI.reset();\r\n }\r\n\r\n $scope.$watch('beforeDate', function () {\r\n if (initializingBefore) {\r\n initializingBefore = false;\r\n } else {\r\n //updateStats();\r\n }\r\n });\r\n\r\n\r\n $scope.$watch('afterDate', function () {\r\n if (initializingAfter) {\r\n initializingAfter = false;\r\n } else {\r\n //updateStats();\r\n }\r\n });\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n //updateStats();\r\n }\r\n };\r\n\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n\r\n $scope.setStats = function (stats) {\r\n //Only update those stats that were calculated (because this might be an update when one stat has just been enabled)\r\n _.forEach(stats, function (value, key) {\r\n if (value !== null) {\r\n $scope.stats[key] = value;\r\n }\r\n });\r\n\r\n if ($scope.stats.avgResponseTimes) {\r\n $scope.avgResponseTimesChart = getChart(\"multiBarHorizontalChart\", $scope.stats.avgResponseTimes, \"indexer\", \"avgResponseTime\", \"\", \"Response time (ms)\");\r\n $scope.avgResponseTimesChart.options.chart.margin.left = 100;\r\n $scope.avgResponseTimesChart.options.chart.yAxis.rotateLabels = -30;\r\n $scope.avgResponseTimesChart.options.chart.height = Math.max($scope.stats.avgResponseTimes.length * 30, 350);\r\n }\r\n\r\n if ($scope.stats.downloadsPerHourOfDay) {\r\n $scope.downloadsPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.stats.downloadsPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Downloads');\r\n $scope.downloadsPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.downloadsPerDayOfWeek) {\r\n $scope.downloadsPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.stats.downloadsPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Downloads');\r\n $scope.downloadsPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.searchesPerHourOfDay) {\r\n $scope.searchesPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.stats.searchesPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Searches');\r\n $scope.searchesPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.searchesPerDayOfWeek) {\r\n $scope.searchesPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.stats.searchesPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Searches');\r\n $scope.searchesPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.downloadsPerAgeStats) {\r\n $scope.downloadsPerAgeChart = getChart(\"discreteBarChart\", $scope.stats.downloadsPerAgeStats.downloadsPerAge, \"age\", \"count\", \"Downloads per age\", 'Downloads');\r\n $scope.downloadsPerAgeChart.options.chart.xAxis.rotateLabels = 45;\r\n $scope.downloadsPerAgeChart.options.chart.showValues = false;\r\n }\r\n\r\n if ($scope.stats.successfulDownloadsPerIndexer) {\r\n $scope.successfulDownloadsPerIndexerChart = getChart(\"multiBarHorizontalChart\", $scope.stats.successfulDownloadsPerIndexer, \"indexerName\", \"percentSuccessful\", \"Indexer\", '% successful');\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.xAxis.rotateLabels = 90;\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.yAxis.tickFormat = function (d) {\r\n return $filter('number')(d, 0);\r\n };\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.valueFormat = function (d) {\r\n return $filter('number')(d, 0);\r\n };\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.showValues = true;\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.margin.left = 80;\r\n }\r\n\r\n if ($scope.stats.indexerDownloadShares) {\r\n $scope.indexerDownloadSharesChart = {\r\n options: {\r\n chart: {\r\n type: 'pieChart',\r\n height: 500,\r\n x: function (d) {\r\n return d.indexerName;\r\n },\r\n y: function (d) {\r\n return d.share;\r\n },\r\n showLabels: true,\r\n donut: true,\r\n donutRatio: 0.35,\r\n duration: 500,\r\n labelThreshold: 0.03,\r\n labelSunbeamLayout: true,\r\n tooltip: {\r\n valueFormatter: function (d, i) {\r\n return $filter('number')(d, 2) + \"%\";\r\n }\r\n },\r\n legend: {\r\n margin: {\r\n top: 5,\r\n right: 35,\r\n bottom: 5,\r\n left: 0\r\n }\r\n }\r\n }\r\n },\r\n data: $scope.stats.indexerDownloadShares\r\n };\r\n $scope.indexerDownloadSharesChart.options.chart.height = Math.min(Math.max(($scope.foo.includeDisabledIndexersInStats ? $scope.stats.numberOfConfiguredIndexers : $scope.stats.numberOfEnabledIndexers) * 40, 350), 900);\r\n }\r\n\r\n function getSharesPieChart(data, height, xValue, yValue) {\r\n return {\r\n options: {\r\n chart: {\r\n type: 'pieChart',\r\n height: height,\r\n x: function (d) {\r\n return d[xValue];\r\n },\r\n y: function (d) {\r\n return d[yValue];\r\n },\r\n showLabels: true,\r\n donut: true,\r\n donutRatio: 0.35,\r\n duration: 500,\r\n labelThreshold: 0.03,\r\n labelsOutside: true,\r\n //labelType: \"percent\",\r\n labelSunbeamLayout: true,\r\n tooltip: {\r\n valueFormatter: function (d, i) {\r\n return $filter('number')(d, 2) + \"%\";\r\n }\r\n },\r\n legend: {\r\n margin: {\r\n top: 5,\r\n right: 35,\r\n bottom: 5,\r\n left: 0\r\n }\r\n }\r\n }\r\n },\r\n data: data\r\n };\r\n }\r\n\r\n if ($scope.stats.searchSharesPerIp !== null) {\r\n $scope.downloadSharesPerIpChart = getSharesPieChart($scope.stats.downloadSharesPerIp, 300, \"key\", \"percentage\");\r\n }\r\n if ($scope.stats.searchSharesPerIpChart !== null) {\r\n $scope.searchSharesPerIpChart = getSharesPieChart($scope.stats.searchSharesPerIp, 300, \"key\", \"percentage\");\r\n }\r\n if ($scope.stats.searchSharesPerUser !== null) {\r\n $scope.downloadSharesPerUserChart = getSharesPieChart($scope.stats.downloadSharesPerUser, 300, \"key\", \"percentage\");\r\n }\r\n if ($scope.stats.searchSharesPerUserChart !== null) {\r\n $scope.searchSharesPerUserChart = getSharesPieChart($scope.stats.searchSharesPerUser, 300, \"key\", \"percentage\");\r\n }\r\n\r\n if ($scope.stats.userAgentSearchShares) {\r\n $scope.userAgentSearchSharesChart = getSharesPieChart($scope.stats.userAgentSearchShares, 300, \"userAgent\", \"percentage\");\r\n $scope.userAgentSearchSharesChart.options.chart.legend.margin.bottom = 25;\r\n }\r\n if ($scope.stats.userAgentDownloadShares) {\r\n $scope.userAgentDownloadSharesChart = getSharesPieChart($scope.stats.userAgentDownloadShares, 300, \"userAgent\", \"percentage\");\r\n $scope.userAgentDownloadSharesChart.options.chart.legend.margin.bottom = 25;\r\n }\r\n\r\n };\r\n\r\n function getChart(chartType, values, xKey, yKey, xAxisLabel, yAxisLabel) {\r\n return {\r\n options: {\r\n chart: {\r\n type: chartType,\r\n height: 350,\r\n margin: {\r\n top: 20,\r\n right: 20,\r\n bottom: 100,\r\n left: 50\r\n },\r\n x: function (d) {\r\n return d[xKey];\r\n },\r\n y: function (d) {\r\n return d[yKey];\r\n },\r\n showValues: true,\r\n valueFormat: function (d) {\r\n return d;\r\n },\r\n color: function () {\r\n return \"red\"\r\n },\r\n showControls: false,\r\n showLegend: false,\r\n duration: 100,\r\n xAxis: {\r\n axisLabel: xAxisLabel,\r\n tickFormat: function (d) {\r\n return d;\r\n },\r\n rotateLabels: 30,\r\n showMaxMin: false,\r\n color: function () {\r\n return \"white\"\r\n }\r\n },\r\n yAxis: {\r\n axisLabel: yAxisLabel,\r\n axisLabelDistance: -10,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n },\r\n tooltip: {\r\n enabled: false\r\n },\r\n zoom: {\r\n enabled: true,\r\n scaleExtent: [1, 10],\r\n useFixedDomain: false,\r\n useNiceScale: false,\r\n horizontalOff: false,\r\n verticalOff: true,\r\n unzoomEventType: 'dblclick.zoom'\r\n }\r\n }\r\n }, data: [{\r\n \"key\": \"doesntmatter\",\r\n \"bar\": true,\r\n \"values\": values\r\n }]\r\n };\r\n }\r\n}\r\n","//\nSearchService.$inject = [\"$http\"];\nangular\n .module('nzbhydraApp')\n .factory('SearchService', SearchService);\n\nfunction SearchService($http) {\n\n\n var lastExecutedQuery;\n var lastExecutedSearchRequestParameters;\n var lastResults;\n var modalInstance;\n\n return {\n search: search,\n getLastResults: getLastResults,\n loadMore: loadMore,\n getModalInstance: getModalInstance,\n setModalInstance: setModalInstance,\n };\n\n function getModalInstance() {\n return modalInstance;\n }\n\n function setModalInstance(mi) {\n modalInstance = mi;\n }\n\n function search(searchRequestId, category, query, metaData, season, episode, minsize, maxsize, minage, maxage, indexers, mode) {\n // console.time(\"search\");\n var uri = new URI(\"internalapi/search\");\n var searchRequestParameters = {};\n searchRequestParameters.searchRequestId = searchRequestId;\n searchRequestParameters.query = query;\n searchRequestParameters.minsize = minsize;\n searchRequestParameters.maxsize = maxsize;\n searchRequestParameters.minage = minage;\n searchRequestParameters.maxage = maxage;\n searchRequestParameters.category = category;\n searchRequestParameters.mode = mode;\n if (!angular.isUndefined(indexers) && indexers !== null) {\n searchRequestParameters.indexers = indexers.split(\",\");\n }\n\n if (metaData) {\n searchRequestParameters.title = metaData.title;\n if (category.indexOf(\"Movies\") > -1 || (category.indexOf(\"20\") === 0) || mode === \"movie\") {\n searchRequestParameters.tmdbId = metaData.tmdbId;\n searchRequestParameters.imdbId = metaData.imdbId;\n } else if (category.indexOf(\"TV\") > -1 || (category.indexOf(\"50\") === 0) || mode === \"tvsearch\") {\n searchRequestParameters.tvdbId = metaData.tvdbId;\n searchRequestParameters.tvrageId = metaData.rid;\n searchRequestParameters.tvmazeId = metaData.tvmazeId;\n searchRequestParameters.season = season;\n searchRequestParameters.episode = episode;\n }\n }\n\n lastExecutedQuery = uri;\n lastExecutedSearchRequestParameters = searchRequestParameters;\n return $http.post(uri.toString(), searchRequestParameters).then(processData);\n }\n\n function loadMore(offset, limit, loadAll) {\n lastExecutedSearchRequestParameters.offset = offset;\n lastExecutedSearchRequestParameters.limit = limit;\n lastExecutedSearchRequestParameters.loadAll = angular.isDefined(loadAll) ? loadAll : false;\n\n return $http.post(lastExecutedQuery.toString(), lastExecutedSearchRequestParameters).then(processData);\n }\n\n\n function processData(response) {\n var searchResults = response.data.searchResults;\n var indexerSearchMetaDatas = response.data.indexerSearchMetaDatas;\n var numberOfAvailableResults = response.data.numberOfAvailableResults;\n var numberOfRejectedResults = response.data.numberOfRejectedResults;\n var numberOfDuplicateResults = response.data.numberOfDuplicateResults;\n var numberOfAcceptedResults = response.data.numberOfAcceptedResults;\n var numberOfProcessedResults = response.data.numberOfProcessedResults;\n var rejectedReasonsMap = response.data.rejectedReasonsMap;\n var notPickedIndexersWithReason = response.data.notPickedIndexersWithReason;\n\n lastResults = {\n \"searchResults\": searchResults,\n \"indexerSearchMetaDatas\": indexerSearchMetaDatas,\n \"numberOfAvailableResults\": numberOfAvailableResults,\n \"numberOfAcceptedResults\": numberOfAcceptedResults,\n \"numberOfRejectedResults\": numberOfRejectedResults,\n \"numberOfProcessedResults\": numberOfProcessedResults,\n \"numberOfDuplicateResults\": numberOfDuplicateResults,\n \"rejectedReasonsMap\": rejectedReasonsMap,\n \"notPickedIndexersWithReason\": notPickedIndexersWithReason\n\n };\n // console.timeEnd(\"searchonly\");\n return lastResults;\n }\n\n function getLastResults() {\n return lastResults;\n }\n}","\nSearchResultsController.$inject = [\"$stateParams\", \"$scope\", \"$q\", \"$timeout\", \"$document\", \"blockUI\", \"growl\", \"localStorageService\", \"SearchService\", \"ConfigService\", \"CategoriesService\", \"DebugService\", \"GenericStorageService\", \"ModalService\", \"$uibModal\"];angular\n .module('nzbhydraApp')\n .controller('SearchResultsController', SearchResultsController);\n\n//SearchResultsController.$inject = ['blockUi'];\nfunction SearchResultsController($stateParams, $scope, $q, $timeout, $document, blockUI, growl, localStorageService, SearchService, ConfigService, CategoriesService, DebugService, GenericStorageService, ModalService, $uibModal) {\n // console.time(\"Presenting\");\n DebugService.log(\"foobar\");\n $scope.limitTo = ConfigService.getSafe().searching.loadLimitInternal;\n $scope.offset = 0;\n $scope.allowZipDownload = ConfigService.getSafe().downloading.fileDownloadAccessType === 'PROXY';\n\n var indexerColors = {};\n\n _.each(ConfigService.getSafe().indexers, function (indexer) {\n indexerColors[indexer.name] = indexer.color;\n });\n\n //Handle incoming data\n\n $scope.indexersearches = SearchService.getLastResults().indexerSearchMetaDatas;\n $scope.notPickedIndexersWithReason = [];\n _.forEach(SearchService.getLastResults().notPickedIndexersWithReason, function (k, v) {\n $scope.notPickedIndexersWithReason.push({\"indexer\": v, \"reason\": k});\n });\n $scope.indexerResultsInfo = {}; //Stores information about the indexerName's searchResults like how many we already retrieved\n $scope.groupExpanded = {};\n $scope.selected = [];\n if ($stateParams.title) {\n $scope.searchTitle = $stateParams.title;\n } else if ($stateParams.query) {\n $scope.searchTitle = $stateParams.query;\n } else {\n $scope.searchTitle = undefined;\n }\n\n $scope.selectedIds = _.map($scope.selected, function (value) {\n return value.searchResultId;\n });\n\n //For shift clicking results\n $scope.lastClickedRowIndex = null;\n $scope.lastClickedValue = null;\n\n var allSearchResults = [];\n var sortModel = {};\n $scope.filterModel = {};\n\n\n $scope.filterButtonsModel = {\n source: {},\n quality: {},\n other: {},\n custom: {}\n };\n $scope.customFilterButtons = [];\n\n $scope.filterButtonsModelMap = {\n tv: ['hdtv'],\n camts: ['cam', 'ts'],\n web: ['webrip', 'web-dl', 'webdl'],\n dvd: ['dvd'],\n bluray: ['bluray', 'blu-ray']\n };\n _.each(ConfigService.getSafe().searching.customQuickFilterButtons, function (entry) {\n var split1 = entry.split(\"=\");\n var displayName = split1[0];\n $scope.filterButtonsModelMap[displayName] = split1[1].split(\",\");\n $scope.customFilterButtons.push(displayName);\n });\n _.each(ConfigService.getSafe().searching.preselectQuickFilterButtons, function (entry) {\n var split1 = entry.split(\"|\");\n var category = split1[0];\n var id = split1[1];\n if (category !== 'source' || $scope.isShowFilterButtonsVideo) {\n $scope.filterButtonsModel[category][id] = true;\n }\n })\n\n $scope.numberOfFilteredResults = 0;\n\n\n if ($stateParams.sortby !== undefined) {\n $stateParams.sortby = $stateParams.sortby.toLowerCase();\n sortModel = {};\n sortModel.reversed = false;\n if ($stateParams.sortby === \"title\") {\n sortModel.column = \"title\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"indexer\") {\n sortModel.column = \"indexer\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"category\") {\n sortModel.column = \"category\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"size\") {\n sortModel.column = \"size\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"details\") {\n sortModel.column = \"grabs\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"age\") {\n sortModel.column = \"epoch\";\n sortModel.reversed = true;\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 2;\n } else {\n sortModel.sortMode = 1;\n }\n }\n\n\n } else if (localStorageService.get(\"sorting\") !== null) {\n sortModel = localStorageService.get(\"sorting\");\n } else {\n sortModel = {\n column: \"epoch\",\n sortMode: 2,\n reversed: false\n };\n }\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode, sortModel.reversed);\n }, 10);\n\n\n $scope.foo = {\n indexerStatusesExpanded: localStorageService.get(\"indexerStatusesExpanded\") !== null ? localStorageService.get(\"indexerStatusesExpanded\") : false,\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") !== null ? localStorageService.get(\"duplicatesDisplayed\") : false,\n groupTorrentAndNewznabResults: localStorageService.get(\"groupTorrentAndNewznabResults\") !== null ? localStorageService.get(\"groupTorrentAndNewznabResults\") : false,\n sumGrabs: localStorageService.get(\"sumGrabs\") !== null ? localStorageService.get(\"sumGrabs\") : true,\n scrollToResults: localStorageService.get(\"scrollToResults\") !== null ? localStorageService.get(\"scrollToResults\") : true,\n showCovers: localStorageService.get(\"showCovers\") !== null ? localStorageService.get(\"showCovers\") : true,\n groupEpisodes: localStorageService.get(\"groupEpisodes\") !== null ? localStorageService.get(\"groupEpisodes\") : true,\n expandGroupsByDefault: localStorageService.get(\"expandGroupsByDefault\") !== null ? localStorageService.get(\"expandGroupsByDefault\") : false,\n showDownloadedIndicator: localStorageService.get(\"showDownloadedIndicator\") !== null ? localStorageService.get(\"showDownloadedIndicator\") : true,\n hideAlreadyDownloadedResults: localStorageService.get(\"hideAlreadyDownloadedResults\") !== null ? localStorageService.get(\"hideAlreadyDownloadedResults\") : true,\n showResultsAsZipButton: localStorageService.get(\"showResultsAsZipButton\") !== null ? localStorageService.get(\"showResultsAsZipButton\") : true,\n alwaysShowTitles: localStorageService.get(\"alwaysShowTitles\") !== null ? localStorageService.get(\"alwaysShowTitles\") : true\n };\n\n\n $scope.isShowFilterButtons = ConfigService.getSafe().searching.showQuickFilterButtons;\n $scope.isShowFilterButtonsVideo = $scope.isShowFilterButtons && ($stateParams.category.toLowerCase().indexOf(\"tv\") > -1 || $stateParams.category.toLowerCase().indexOf(\"movie\") > -1 || ConfigService.getSafe().searching.alwaysShowQuickFilterButtons);\n $scope.isShowCustomFilterButtons = ConfigService.getSafe().searching.customQuickFilterButtons.length > 0;\n\n $scope.shared = {\n isGroupEpisodes: $scope.foo.groupEpisodes && $stateParams.category.toLowerCase().indexOf(\"tv\") > -1 && $stateParams.episode === undefined,\n expandGroupsByDefault: $scope.foo.expandGroupsByDefault,\n showDownloadedIndicator: $scope.foo.showDownloadedIndicator,\n hideAlreadyDownloadedResults: $scope.foo.hideAlreadyDownloadedResults,\n alwaysShowTitles: $scope.foo.alwaysShowTitles\n };\n\n if ($scope.shared.isGroupEpisodes) {\n GenericStorageService.get(\"isGroupEpisodesHelpShown\", true).then(function (response) {\n if (!response.data) {\n ModalService.open(\"Sorting of TV episodes\", 'When searching in the TV categories results are automatically grouped by episodes. This makes it easier to download one episode each. You can disable this feature any time using the \"Display options\" button to the upper left.', {\n yes: {\n text: \"OK\"\n }\n });\n GenericStorageService.put(\"isGroupEpisodesHelpShown\", true, true);\n }\n\n })\n }\n\n $scope.loadMoreEnabled = false;\n $scope.totalAvailableUnknown = false;\n $scope.expandedTitlegroups = [];\n $scope.optionsOptions = [\n {id: \"duplicatesDisplayed\", label: \"Show duplicate display triggers\"},\n {id: \"groupTorrentAndNewznabResults\", label: \"Group torrent and usenet results\"},\n {id: \"sumGrabs\", label: \"Use sum of grabs / seeders for filtering / sorting of groups\"},\n {id: \"scrollToResults\", label: \"Scroll to results when finished\"},\n {id: \"showCovers\", label: \"Show movie covers in results\"},\n {id: \"groupEpisodes\", label: \"Group TV results by season/episode\"},\n {id: \"expandGroupsByDefault\", label: \"Expand groups by default\"},\n {id: \"alwaysShowTitles\", label: \"Always show result titles (even when grouped)\"},\n {id: \"showDownloadedIndicator\", label: \"Show already downloaded indicator\"},\n {id: \"hideAlreadyDownloadedResults\", label: \"Hide already downloaded results\"}\n ];\n if ($scope.allowZipDownload) {\n $scope.optionsOptions.push({id: \"showResultsAsZipButton\", label: \"Show button to download results as ZIP\"});\n }\n $scope.optionsSelectedModel = [];\n for (var key in $scope.optionsOptions) {\n if ($scope.foo[$scope.optionsOptions[key][\"id\"]]) {\n $scope.optionsSelectedModel.push($scope.optionsOptions[key].id);\n }\n }\n\n $scope.optionsExtraSettings = {\n showSelectAll: false,\n showDeselectAll: false,\n buttonText: \"Display options\"\n };\n\n $scope.optionsEvents = {\n onToggleItem: function (item, newValue) {\n if (item.id === \"duplicatesDisplayed\") {\n toggleDuplicatesDisplayed(newValue);\n } else if (item.id === \"groupTorrentAndNewznabResults\") {\n toggleGroupTorrentAndNewznabResults(newValue);\n } else if (item.id === \"sumGrabs\") {\n toggleSumGrabs(newValue);\n } else if (item.id === \"scrollToResults\") {\n toggleScrollToResults(newValue);\n } else if (item.id === \"showCovers\") {\n toggleShowCovers(newValue);\n } else if (item.id === \"groupEpisodes\") {\n toggleGroupEpisodes(newValue);\n } else if (item.id === \"expandGroupsByDefault\") {\n toggleExpandGroups(newValue);\n } else if (item.id === \"showDownloadedIndicator\") {\n toggleDownloadedIndicator(newValue);\n } else if (item.id === \"hideAlreadyDownloadedResults\") {\n toggleHideAlreadyDownloadedResults(newValue);\n } else if (item.id === \"showResultsAsZipButton\") {\n toggleShowResultsAsZipButton(newValue);\n } else if (item.id === \"alwaysShowTitles\") {\n toggleAlwaysShowTitles(newValue);\n }\n }\n };\n\n function toggleDuplicatesDisplayed(value) {\n localStorageService.set(\"duplicatesDisplayed\", value);\n $scope.$broadcast(\"duplicatesDisplayed\", value);\n $scope.foo.duplicatesDisplayed = value;\n $scope.shared.duplicatesDisplayed = value;\n }\n\n function toggleGroupTorrentAndNewznabResults(value) {\n localStorageService.set(\"groupTorrentAndNewznabResults\", value);\n $scope.foo.groupTorrentAndNewznabResults = value;\n $scope.shared.groupTorrentAndNewznabResults = value;\n blockAndUpdate();\n }\n\n function toggleSumGrabs(value) {\n localStorageService.set(\"sumGrabs\", value);\n $scope.foo.sumGrabs = value;\n $scope.shared.sumGrabs = value;\n blockAndUpdate();\n }\n\n function toggleScrollToResults(value) {\n localStorageService.set(\"scrollToResults\", value);\n $scope.foo.scrollToResults = value;\n $scope.shared.scrollToResults = value;\n }\n\n function toggleShowCovers(value) {\n localStorageService.set(\"showCovers\", value);\n $scope.foo.showCovers = value;\n $scope.shared.showCovers = value;\n $scope.$broadcast(\"toggleShowCovers\", value);\n }\n\n function toggleGroupEpisodes(value) {\n localStorageService.set(\"groupEpisodes\", value);\n $scope.shared.isGroupEpisodes = value;\n $scope.foo.isGroupEpisodes = value;\n blockAndUpdate();\n }\n\n function toggleExpandGroups(value) {\n localStorageService.set(\"expandGroupsByDefault\", value);\n $scope.shared.isExpandGroupsByDefault = value;\n $scope.foo.isExpandGroupsByDefault = value;\n blockAndUpdate();\n }\n\n function toggleDownloadedIndicator(value) {\n localStorageService.set(\"showDownloadedIndicator\", value);\n $scope.shared.showDownloadedIndicator = value;\n $scope.foo.showDownloadedIndicator = value;\n blockAndUpdate();\n }\n\n function toggleHideAlreadyDownloadedResults(value) {\n localStorageService.set(\"hideAlreadyDownloadedResults\", value);\n $scope.foo.hideAlreadyDownloadedResults = value;\n blockAndUpdate();\n }\n\n function toggleShowResultsAsZipButton(value) {\n localStorageService.set(\"showResultsAsZipButton\", value);\n $scope.shared.showResultsAsZipButton = value;\n $scope.foo.showResultsAsZipButton = value;\n }\n\n function toggleAlwaysShowTitles(value) {\n localStorageService.set(\"alwaysShowTitles\", value);\n $scope.shared.alwaysShowTitles = value;\n $scope.foo.alwaysShowTitles = value;\n $scope.$broadcast(\"toggleAlwaysShowTitles\", value);\n }\n\n\n $scope.indexersForFiltering = [];\n _.forEach($scope.indexersearches, function (indexer) {\n $scope.indexersForFiltering.push({label: indexer.indexerName, id: indexer.indexerName})\n });\n $scope.categoriesForFiltering = [];\n _.forEach(CategoriesService.getWithoutAll(), function (category) {\n $scope.categoriesForFiltering.push({label: category.name, id: category.name})\n });\n _.forEach($scope.indexersearches, function (ps) {\n $scope.indexerResultsInfo[ps.indexerName.toLowerCase()] = {loadedResults: ps.loaded_results};\n });\n\n setDataFromSearchResult(SearchService.getLastResults(), []);\n $scope.$emit(\"searchResultsShown\");\n\n if (!SearchService.getLastResults().searchResults || SearchService.getLastResults().searchResults.length === 0 || $scope.allResultsFiltered || $scope.numberOfAcceptedResults === 0) {\n //Close modal instance because no search results will be rendered that could trigger the closing\n console.log(\"CLosing status window\");\n SearchService.getModalInstance().close();\n $scope.doShowResults = true;\n } else {\n console.log(\"Will leave the closing of the status window to finishRendering. # of search results: \" + SearchService.getLastResults().searchResults.length + \". All results filtered: \" + $scope.allResultsFiltered);\n }\n\n //Returns the content of the property (defined by the current sortPredicate) of the first group element\n $scope.firstResultPredicate = firstResultPredicate;\n\n function firstResultPredicate(item) {\n return item[0][$scope.sortPredicate];\n }\n\n //Returns the unique group identifier which allows angular to keep track of the grouped search results even after filtering, making filtering by indexers a lot faster (albeit still somewhat slow...)\n $scope.groupId = groupId;\n\n function groupId(item) {\n return item[0][0].searchResultId;\n }\n\n $scope.onFilterButtonsModelChange = function () {\n console.log($scope.filterButtonsModel);\n blockAndUpdate();\n };\n\n function blockAndUpdate() {\n startBlocking(\"Sorting / filtering...\").then(function () {\n [$scope.filteredResults, $scope.filterReasons] = sortAndFilter(allSearchResults);\n localStorageService.set(\"sorting\", sortModel);\n });\n }\n\n //Block the UI and return after timeout. This way we make sure that the blocking is done before angular starts updating the model/view. There's probably a better way to achieve that?\n function startBlocking(message) {\n var deferred = $q.defer();\n blockUI.start(message);\n $timeout(function () {\n deferred.resolve();\n }, 10);\n return deferred.promise;\n }\n\n $scope.$on(\"sort\", function (event, column, sortMode, reversed) {\n if (sortMode === 0) {\n sortModel = {\n column: \"epoch\",\n sortMode: 2,\n reversed: true\n };\n } else {\n sortModel = {\n column: column,\n sortMode: sortMode,\n reversed: reversed\n };\n }\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode, sortModel.reversed);\n }, 10);\n blockAndUpdate();\n });\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filterValue && isActive) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n blockAndUpdate();\n });\n\n $scope.resort = function () {\n };\n\n function getCleanedTitle(element) {\n try {\n return element.title.toLowerCase().replace(/[\\s\\-\\._]/ig, \"\");\n } catch (e) {\n console.error(\"Unable to clean title for result \" + element);\n }\n }\n\n function getGroupingString(element) {\n\n var groupingString;\n if ($scope.shared.isGroupEpisodes) {\n groupingString = (element.showtitle + \"x\" + element.season + \"x\" + element.episode).toLowerCase().replace(/[\\._\\-]/ig, \"\");\n if (groupingString === \"nullxnullxnull\") {\n groupingString = getCleanedTitle(element);\n }\n } else {\n groupingString = getCleanedTitle(element);\n if (!$scope.foo.groupTorrentAndNewznabResults) {\n groupingString = groupingString + element.downloadType;\n }\n }\n return groupingString;\n }\n\n function sortAndFilter(results) {\n var query;\n var words;\n var filterReasons = {\n \"tooSmall\": 0,\n \"tooLarge\": 0,\n \"tooYoung\": 0,\n \"tooOld\": 0,\n \"tooFewGrabs\": 0,\n \"tooManyGrabs\": 0,\n \"title\": 0,\n \"tooindexer\": 0,\n \"category\": 0,\n \"tooOld\": 0,\n \"quickFilter\": 0,\n \"alreadyDownloaded\": 0\n\n\n };\n\n if (\"title\" in $scope.filterModel) {\n query = $scope.filterModel.title.filterValue;\n if (!(query.startsWith(\"/\") && query.endsWith(\"/\"))) {\n words = query.toLowerCase().split(/[\\s.\\-]+/);\n }\n }\n\n function filter(item) {\n if (item.title === null || item.title === undefined) {\n //https://github.com/theotherp/nzbhydra2/issues/690\n console.error(\"Item without title: \" + JSON.stringify(item))\n }\n if (\"size\" in $scope.filterModel) {\n var filterValue = $scope.filterModel.size.filterValue;\n if (angular.isDefined(filterValue.min) && item.size / 1024 / 1024 < filterValue.min) {\n filterReasons[\"tooSmall\"] = filterReasons[\"tooSmall\"] + 1;\n return false;\n }\n if (angular.isDefined(filterValue.max) && item.size / 1024 / 1024 > filterValue.max) {\n filterReasons[\"tooLarge\"] = filterReasons[\"tooLarge\"] + 1;\n return false;\n }\n }\n\n if (\"epoch\" in $scope.filterModel) {\n var filterValue = $scope.filterModel.epoch.filterValue;\n var ageDays = moment.utc().diff(moment.unix(item.epoch), \"days\");\n if (angular.isDefined(filterValue.min) && ageDays < filterValue.min) {\n filterReasons[\"tooYoung\"] = filterReasons[\"tooYoung\"] + 1;\n return false;\n }\n if (angular.isDefined(filterValue.max) && ageDays > filterValue.max) {\n filterReasons[\"tooOld\"] = filterReasons[\"tooOld\"] + 1;\n return false;\n }\n }\n\n if (\"grabs\" in $scope.filterModel) {\n var filterValue = $scope.filterModel.grabs.filterValue;\n if (angular.isDefined(filterValue.min)) {\n if ((item.seeders !== null && item.seeders < filterValue.min) || (item.seeders === null && item.grabs !== null && item.grabs < filterValue.min)) {\n filterReasons[\"tooFewGrabs\"] = filterReasons[\"tooFewGrabs\"] + 1;\n return false;\n }\n }\n if (angular.isDefined(filterValue.max)) {\n if ((item.seeders !== null && item.seeders > filterValue.max) || (item.seeders === null && item.grabs !== null && item.grabs > filterValue.max)) {\n filterReasons[\"tooManyGrabs\"] = filterReasons[\"tooManyGrabs\"] + 1;\n return false;\n }\n }\n }\n\n if (\"title\" in $scope.filterModel) {\n var ok;\n if (query.startsWith(\"/\") && query.endsWith(\"/\")) {\n ok = item.title.toLowerCase().match(new RegExp(query.substr(1, query.length - 2), \"gi\"));\n } else {\n ok = _.every(words, function (word) {\n if (word.startsWith(\"!\")) {\n if (word.length === 1) {\n return true;\n }\n return item.title.toLowerCase().indexOf(word.substring(1)) === -1;\n }\n return item.title.toLowerCase().indexOf(word) > -1;\n });\n }\n if (!ok) {\n filterReasons[\"title\"] = filterReasons[\"title\"] + 1;\n return false;\n }\n }\n if (\"indexer\" in $scope.filterModel) {\n if (_.indexOf($scope.filterModel.indexer.filterValue, item.indexer) === -1) {\n filterReasons[\"title\"] = filterReasons[\"title\"] + 1;\n return false;\n }\n }\n if (\"category\" in $scope.filterModel) {\n if (_.indexOf($scope.filterModel.category.filterValue, item.category) === -1) {\n filterReasons[\"category\"] = filterReasons[\"category\"] + 1;\n return false;\n }\n }\n if ($scope.filterButtonsModel.source !== null) {\n var mustContain = [];\n _.each($scope.filterButtonsModel.source, function (value, key) { //key is something like 'camts', value is true or false\n if (value) {\n Array.prototype.push.apply(mustContain, $scope.filterButtonsModelMap[key]);\n }\n });\n if (mustContain.length > 0) {\n var containsAtLeastOne = _.any(mustContain, function (word) {\n return item.title.toLowerCase().indexOf(word.toLowerCase()) > -1\n });\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the words \" + JSON.stringify(mustContain));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n }\n if ($scope.filterButtonsModel.quality !== null && !_.isEmpty($scope.filterButtonsModel.quality)) {\n //key is something like 'q720p', value is true or false.\n var requiresAnyOf = _.keys(_.pick($scope.filterButtonsModel.quality, function (value, key) {\n return value\n }));\n if (requiresAnyOf.length === 0) {\n return true;\n }\n\n var containsAtLeastOne = _.any(requiresAnyOf, function (required) {\n if (item.title.toLowerCase().indexOf(required.substring(1).toLowerCase()) > -1) {\n //We need to remove the \"q\" which is there because keys may not start with a digit\n return true;\n }\n })\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the qualities \" + JSON.stringify(requiresAnyOf));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n if ($scope.filterButtonsModel.other !== null && !_.isEmpty($scope.filterButtonsModel.other)) {\n var requiresAnyOf = _.keys(_.pick($scope.filterButtonsModel.other, function (value, key) {\n return value\n }));\n if (requiresAnyOf.length === 0) {\n return true;\n }\n var containsAtLeastOne = _.any(requiresAnyOf, function (required) {\n if (item.title.toLowerCase().indexOf(required.toLowerCase()) > -1) {\n return true;\n }\n })\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the 'other' values \" + JSON.stringify(requiresAnyOf));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n if ($scope.filterButtonsModel.custom !== null && !_.isEmpty($scope.filterButtonsModel.custom)) {\n var requiresAnyOf = _.keys(_.pick($scope.filterButtonsModel.custom, function (value, key) {\n return value\n }));\n if (requiresAnyOf.length === 0) {\n return true;\n }\n var containsAtLeastOne = _.any(requiresAnyOf, function (required) {\n if (item.title.toLowerCase().indexOf(required.toLowerCase()) > -1) {\n return true;\n }\n })\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the custom values' \" + JSON.stringify(requiresAnyOf));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n\n if ($scope.foo.hideAlreadyDownloadedResults && item.downloadedAt !== null) {\n filterReasons[\"alreadyDownloaded\"] = filterReasons[\"alreadyDownloaded\"] + 1;\n return false;\n }\n\n return true;\n }\n\n\n var sortPredicateKey = sortModel.column;\n var sortReversed = sortModel.reversed;\n\n function getSortPredicateValue(containgObject) {\n var sortPredicateValue;\n if (sortPredicateKey === \"grabs\") {\n if (containgObject[\"seeders\"] !== null) {\n sortPredicateValue = containgObject[\"seeders\"];\n } else if (containgObject[\"grabs\"] !== null) {\n sortPredicateValue = containgObject[\"grabs\"];\n } else {\n sortPredicateValue = 0;\n }\n } else if (sortPredicateKey === \"title\") {\n sortPredicateValue = getCleanedTitle(containgObject);\n } else if (sortPredicateKey === \"indexer\") {\n sortPredicateValue = containgObject[\"indexer\"].toLowerCase();\n } else {\n sortPredicateValue = containgObject[sortPredicateKey];\n }\n return sortPredicateValue;\n }\n\n function createSortedHashgroups(titleGroup) {\n function createHashGroup(hashGroup) {\n //Sorting hash group's contents should not matter for size and age and title but might for category (we might remove this, it's probably mostly unnecessary)\n var sortedHashGroup = _.sortBy(hashGroup, function (item) {\n var sortPredicateValue = getSortPredicateValue(item);\n return sortReversed ? -sortPredicateValue : sortPredicateValue;\n });\n //Now sort the hash group by indexer score (inverted) so that the result with the highest indexer score is shown on top (or as the only one of a hash group if it's collapsed)\n sortedHashGroup = _.sortBy(sortedHashGroup, function (item) {\n return item.indexerscore * -1;\n });\n return sortedHashGroup;\n }\n\n function getHashGroupFirstElementSortPredicate(hashGroup) {\n if (sortPredicateKey === \"title\") {\n //Sorting a title group internally by title doesn't make sense so fall back to sorting by age so that newest result is at the top\n return ((10000000000 * hashGroup[0][\"indexerscore\"]) + hashGroup[0][\"epoch\"]) * -1;\n }\n return getSortPredicateValue(hashGroup[0]);\n }\n\n var grouped = _.groupBy(titleGroup, \"hash\");\n var mapped = _.map(grouped, createHashGroup);\n var sorted = _.sortBy(mapped, getHashGroupFirstElementSortPredicate);\n if (sortModel.sortMode === 2 && sortPredicateKey !== \"title\") {\n sorted = sorted.reverse();\n }\n\n return sorted;\n }\n\n function getTitleGroupFirstElementsSortPredicate(titleGroup) {\n var sortPredicateValue;\n if (sortPredicateKey === \"grabs\" && $scope.foo.sumGrabs) {\n var sumOfGrabs = 0;\n _.each(titleGroup, function (element1) {\n _.each(element1, function (element2) {\n sumOfGrabs += getSortPredicateValue(element2);\n })\n });\n\n sortPredicateValue = sumOfGrabs;\n } else {\n sortPredicateValue = getSortPredicateValue(titleGroup[0][0]);\n }\n return sortPredicateValue\n }\n\n _.each(results, function (result) {\n var indexerColor = indexerColors[result.indexer];\n if (indexerColor === undefined || indexerColor === null) {\n return \"\";\n }\n result.style = \"background-color: \" + indexerColor.replace(\"rgb\", \"rgba\").replace(\")\", \",0.5)\")\n });\n\n var filtered = _.filter(results, filter);\n $scope.numberOfFilteredResults = results.length - filtered.length;\n $scope.allResultsFiltered = results.length > 0 && ($scope.numberOfFilteredResults === results.length);\n console.log(\"Filtered \" + $scope.numberOfFilteredResults + \" out of \" + results.length);\n var newSelected = $scope.selected;\n _.forEach($scope.selected, function (x) {\n if (x === undefined) {\n return;\n }\n if (filtered.indexOf(x) === -1) {\n $scope.$broadcast(\"toggleSelection\", x, false);\n newSelected.splice($scope.selected.indexOf(x), 1);\n }\n });\n $scope.selected = newSelected;\n\n var grouped = _.groupBy(filtered, getGroupingString);\n\n var mapped = _.map(grouped, createSortedHashgroups);\n var sorted = _.sortBy(mapped, getTitleGroupFirstElementsSortPredicate);\n if (sortModel.sortMode === 2) {\n sorted = sorted.reverse();\n }\n\n $scope.lastClickedRowIndex = null;\n\n var filteredResults = [];\n var countTitleGroups = 0;\n var countResultsUntilTitleGroupLimitReached = 0;\n _.forEach(sorted, function (titleGroup) {\n var titleGroupIndex = 0;\n countTitleGroups++;\n\n _.forEach(titleGroup, function (duplicateGroup) {\n var duplicateIndex = 0;\n _.forEach(duplicateGroup, function (result) {\n try {\n result.titleGroupIndicator = getGroupingString(result);\n result.titleGroupIndex = titleGroupIndex;\n result.duplicateGroupIndex = duplicateIndex;\n result.duplicatesLength = duplicateGroup.length;\n result.titlesLength = titleGroup.length;\n filteredResults.push(result);\n duplicateIndex += 1;\n if (countTitleGroups <= $scope.limitTo) {\n countResultsUntilTitleGroupLimitReached++;\n }\n if (duplicateGroup.length > 1)\n $scope.countDuplicates += (duplicateGroup.length - 1)\n } catch (e) {\n console.error(\"Error while processing result \" + result, e);\n }\n });\n titleGroupIndex += 1;\n });\n });\n $scope.limitTo = Math.max($scope.limitTo, countResultsUntilTitleGroupLimitReached);\n\n $scope.$broadcast(\"calculateDisplayState\");\n\n return [filteredResults, filterReasons];\n }\n\n $scope.toggleTitlegroupExpand = function toggleTitlegroupExpand(titleGroup) {\n $scope.groupExpanded[titleGroup[0][0].title] = !$scope.groupExpanded[titleGroup[0][0].title];\n $scope.groupExpanded[titleGroup[0][0].hash] = !$scope.groupExpanded[titleGroup[0][0].hash];\n };\n\n $scope.stopBlocking = stopBlocking;\n\n function stopBlocking() {\n blockUI.reset();\n }\n\n function setDataFromSearchResult(data, previousSearchResults) {\n allSearchResults = previousSearchResults.concat(data.searchResults);\n allSearchResults = uniq(allSearchResults);\n [$scope.filteredResults, $scope.filterReasons] = sortAndFilter(allSearchResults);\n\n $scope.numberOfAvailableResults = data.numberOfAvailableResults;\n $scope.rejectedReasonsMap = data.rejectedReasonsMap;\n $scope.anyResultsRejected = !_.isEmpty(data.rejectedReasonsMap);\n $scope.anyIndexersSearchedSuccessfully = _.any(data.indexerSearchMetaDatas, function (x) {\n return x.wasSuccessful;\n });\n $scope.numberOfAcceptedResults = data.numberOfAcceptedResults;\n $scope.numberOfRejectedResults = data.numberOfRejectedResults;\n $scope.numberOfProcessedResults = data.numberOfProcessedResults;\n $scope.numberOfDuplicateResults = data.numberOfDuplicateResults;\n $scope.numberOfLoadedResults = allSearchResults.length;\n $scope.indexersearches = data.indexerSearchMetaDatas;\n\n $scope.loadMoreEnabled = ($scope.numberOfLoadedResults + $scope.numberOfRejectedResults < $scope.numberOfAvailableResults) || _.any(data.indexerSearchMetaDatas, function (x) {\n return x.hasMoreResults;\n });\n $scope.totalAvailableUnknown = _.any(data.indexerSearchMetaDatas, function (x) {\n return !x.totalResultsKnown;\n });\n\n if (!$scope.foo.indexerStatusesExpanded && _.any(data.indexerSearchMetaDatas, function (x) {\n return !x.wasSuccessful;\n })) {\n growl.info(\"Errors occurred during searching, Check indexer statuses\")\n }\n //Only show those categories in filter that are actually present in the results\n $scope.categoriesForFiltering = [];\n var allUsedCategories = _.uniq(_.pluck(allSearchResults, \"category\"));\n _.forEach(CategoriesService.getWithoutAll(), function (category) {\n if (allUsedCategories.indexOf(category.name) > -1) {\n $scope.categoriesForFiltering.push({label: category.name, id: category.name})\n }\n });\n }\n\n function uniq(searchResults) {\n var seen = {};\n var out = [];\n var len = searchResults.length;\n var j = 0;\n for (var i = 0; i < len; i++) {\n var item = searchResults[i];\n if (seen[item.searchResultId] !== 1) {\n seen[item.searchResultId] = 1;\n out[j++] = item;\n }\n }\n return out;\n }\n\n $scope.loadMore = loadMore;\n\n function loadMore(loadAll) {\n startBlocking(loadAll ? \"Loading all results...\" : \"Loading more results...\").then(function () {\n $scope.loadingMore = true;\n var limit = loadAll ? $scope.numberOfAvailableResults - $scope.numberOfProcessedResults : null;\n SearchService.loadMore($scope.numberOfLoadedResults, limit, loadAll).then(function (data) {\n setDataFromSearchResult(data, allSearchResults);\n $scope.loadingMore = false;\n //stopBlocking();\n });\n });\n }\n\n\n $scope.countResults = countResults;\n\n function countResults() {\n return allSearchResults.length;\n }\n\n $scope.invertSelection = function invertSelection() {\n $scope.$broadcast(\"invertSelection\");\n };\n\n $scope.deselectAll = function deselectAll() {\n $scope.$broadcast(\"deselectAll\");\n };\n\n $scope.selectAll = function selectAll() {\n $scope.$broadcast(\"selectAll\");\n };\n\n $scope.toggleIndexerStatuses = function () {\n $scope.foo.indexerStatusesExpanded = !$scope.foo.indexerStatusesExpanded;\n localStorageService.set(\"indexerStatusesExpanded\", $scope.foo.indexerStatusesExpanded);\n };\n\n $scope.getRejectedReasonsTooltip = function () {\n if (_.isEmpty($scope.rejectedReasonsMap)) {\n return \"No rejected results\";\n } else {\n var tooltip = \"Rejected results:
          \";\n tooltip += '';\n _.forEach($scope.rejectedReasonsMap, function (count, reason) {\n tooltip += '';\n });\n tooltip += '
          CountReason
          ' + count + '' + reason + '
          ';\n tooltip += '
          ';\n tooltip += \"Filtered results:
          \";\n tooltip += '';\n _.forEach($scope.filterReasons, function (count, reason) {\n if (count > 0) {\n tooltip += '';\n }\n });\n tooltip += '
          CountReason
          ' + count + '' + reason + '
          ';\n tooltip += '
          '\n return tooltip;\n }\n };\n\n\n $scope.$on(\"checkboxClicked\", function (event, originalEvent, rowIndex, newCheckedValue, clickTargetElement) {\n if (originalEvent.shiftKey && $scope.lastClickedRowIndex !== null) {\n $scope.$broadcast(\"shiftClick\", Number($scope.lastClickedRowIndex), Number(rowIndex), Number($scope.lastClickedValue), $scope.lastClickedElement, clickTargetElement);\n }\n $scope.lastClickedRowIndex = rowIndex;\n $scope.lastClickedElement = clickTargetElement;\n $scope.lastClickedValue = newCheckedValue;\n });\n\n $scope.$on(\"toggleTitleExpansionUp\", function ($event, value, titleGroupIndicator) {\n $scope.$broadcast(\"toggleTitleExpansionDown\", value, titleGroupIndicator);\n });\n\n $scope.$on(\"toggleDuplicateExpansionUp\", function ($event, value, hash) {\n $scope.$broadcast(\"toggleDuplicateExpansionDown\", value, hash);\n });\n\n $scope.$on(\"selectionUp\", function ($event, result, value) {\n var index = $scope.selected.indexOf(result);\n if (value && index === -1) {\n $scope.selected.push(result);\n } else if (!value && index > -1) {\n $scope.selected.splice(index, 1);\n }\n });\n\n $scope.downloadNzbsCallback = function (addedIds) {\n if (addedIds !== null && addedIds.length > 0) {\n growl.info(\"Removing downloaded NZBs from selection\");\n var toRemove = _.filter($scope.selected, function (x) {\n return addedIds.indexOf(Number(x.searchResultId)) > -1;\n });\n var newSelected = $scope.selected;\n _.forEach(toRemove, function (x) {\n $scope.$broadcast(\"toggleSelection\", x, false);\n newSelected.splice($scope.selected.indexOf(x), 1);\n });\n $scope.selected = newSelected;\n }\n };\n\n\n $scope.filterRejectedZero = function () {\n return function (entry) {\n return entry[1] > 0;\n }\n };\n\n $scope.onPageChange = function (newPageNumber, oldPageNumber) {\n _.each($scope.selected, function (x) {\n $scope.$broadcast(\"toggleSelection\", x, true);\n })\n };\n\n $scope.$on(\"onFinishRender\", function () {\n console.log(\"Finished rendering results.\")\n $scope.doShowResults = true;\n $timeout(function () {\n if ($scope.foo.scrollToResults) {\n var searchResultsElement = angular.element(document.getElementById('display-options'));\n $document.scrollToElement(searchResultsElement, 0, 500);\n }\n stopBlocking();\n console.log(\"Closing search status window because rendering is finished.\")\n SearchService.getModalInstance().close();\n }, 1);\n });\n\n\n $timeout(function () {\n DebugService.print();\n }, 3000);\n\n // $timeout(function () {\n // function getWatchers(root) {\n // root = angular.element(root || document.documentElement);\n // var watcherCount = 0;\n // var ids = [];\n //\n // function getElemWatchers(element, ids) {\n // var isolateWatchers = getWatchersFromScope(element.data().$isolateScope, ids);\n // var scopeWatchers = getWatchersFromScope(element.data().$scope, ids);\n // var watchers = scopeWatchers.concat(isolateWatchers);\n // angular.forEach(element.children(), function (childElement) {\n // watchers = watchers.concat(getElemWatchers(angular.element(childElement), ids));\n // });\n // return watchers;\n // }\n //\n // function getWatchersFromScope(scope, ids) {\n // if (scope) {\n // if (_.indexOf(ids, scope.$id) > -1) {\n // return [];\n // }\n // ids.push(scope.$id);\n // if (scope.$$watchers) {\n // if (scope.$$watchers.length > 1) {\n // var a;\n // a = 1;\n // }\n // return scope.$$watchers;\n // }\n // {\n // return [];\n // }\n //\n // } else {\n // return [];\n // }\n // }\n //\n // return getElemWatchers(root, ids);\n // }\n //\n // }, $scope.limitTo);\n}\n","\r\nSearchHistoryService.$inject = [\"$filter\", \"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('SearchHistoryService', SearchHistoryService);\r\n\r\nfunction SearchHistoryService($filter, $http) {\r\n\r\n return {\r\n getSearchHistory: getSearchHistory,\r\n getSearchHistoryForSearching: getSearchHistoryForSearching,\r\n formatRequest: formatRequest,\r\n getStateParamsForRepeatedSearch: getStateParamsForRepeatedSearch\r\n };\r\n\r\n function getSearchHistoryForSearching() {\r\n return $http.post(\"internalapi/history/searches/forsearching\").then(function (response) {\r\n return {\r\n searchRequests: response.data\r\n }\r\n });\r\n }\r\n\r\n function getSearchHistory(pageNumber, limit, filterModel, sortModel, distinct, onlyCurrentUser) {\r\n var params = {\r\n page: pageNumber,\r\n limit: limit,\r\n filterModel: filterModel,\r\n distinct: distinct,\r\n onlyCurrentUser: onlyCurrentUser\r\n };\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n } else {\r\n params.sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n }\r\n return $http.post(\"internalapi/history/searches\", params).then(function (response) {\r\n return {\r\n searchRequests: response.data.content,\r\n totalRequests: response.data.totalElements\r\n }\r\n });\r\n }\r\n\r\n function formatRequest(request, includeIdLink, includequery, describeEmptySearch, includeTitle) {\r\n var result = [];\r\n result.push('Category: ' + request.categoryName);\r\n if (includequery && request.query) {\r\n result.push('Query: ' + request.query);\r\n }\r\n if (request.title && includeTitle) {\r\n result.push('Title: ' + request.title);\r\n } //Only include identifiers if title is unknown\r\n else if (request.identifiers.length > 0) {\r\n var href;\r\n var key;\r\n var value;\r\n var identifiers = _.indexBy(request.identifiers, 'identifierKey');\r\n if (\"IMDB\" in identifiers) {\r\n key = \"IMDB ID\";\r\n value = identifiers.IMDB.identifierValue;\r\n href = \"https://www.imdb.com/title/tt\" + value;\r\n } else if (\"TVDB\" in identifiers) {\r\n key = \"TVDB ID\";\r\n value = identifiers.TVDB.identifierValue;\r\n href = \"https://thetvdb.com/?tab=series&id=\" + value;\r\n } else if (\"TVRAGE\" in identifiers) {\r\n key = \"TVRage ID\";\r\n value = identifiers.TVRAGE.identifierValue;\r\n href = \"internalapi/redirect_rid?rid=\" + value;\r\n } else if (\"TMDB\" in identifiers) {\r\n key = \"TMDB ID\";\r\n value = identifiers.TMDB.identifierValue;\r\n href = \"https://www.themoviedb.org/movie/\" + value;\r\n }\r\n href = $filter(\"dereferer\")(href);\r\n if (includeIdLink) {\r\n result.push('' + key + ': ' + value + \"\");\r\n } else {\r\n result.push('' + key + \": \" + value);\r\n }\r\n }\r\n if (request.season) {\r\n result.push('Season: ' + request.season);\r\n }\r\n if (request.episode) {\r\n result.push('Episode: ' + request.episode);\r\n }\r\n if (request.author) {\r\n result.push('Author: ' + request.author);\r\n }\r\n if (result.length === 0 && describeEmptySearch) {\r\n result = ['Empty search'];\r\n }\r\n\r\n return result.join(\", \");\r\n\r\n }\r\n\r\n function getStateParamsForRepeatedSearch(request) {\r\n var stateParams = {};\r\n stateParams.mode = \"search\";\r\n var availableIdentifiers = _.pluck(request.identifiers, \"identifierKey\");\r\n if (availableIdentifiers.indexOf(\"TMDB\") > -1 || availableIdentifiers.indexOf(\"IMDB\") > -1) {\r\n stateParams.mode = \"movie\";\r\n } else if (availableIdentifiers.indexOf(\"TVRAGE\") > -1 || availableIdentifiers.indexOf(\"TVMAZE\") > -1 || availableIdentifiers.indexOf(\"TVDB\") > -1) {\r\n stateParams.mode = \"tvsearch\";\r\n }\r\n if (request.season) {\r\n stateParams.season = request.season;\r\n }\r\n if (request.episode) {\r\n stateParams.episode = request.episode;\r\n }\r\n\r\n _.each(request.identifiers, function (entry) {\r\n switch (entry.identifierKey) {\r\n case \"TMDB\":\r\n stateParams.tmdbId = entry.identifierValue;\r\n break;\r\n case \"IMDB\":\r\n stateParams.imdbId = entry.identifierValue;\r\n break;\r\n case \"TVMAZE\":\r\n stateParams.tvmazeId = entry.identifierValue;\r\n break;\r\n case \"TVRAGE\":\r\n stateParams.tvrageId = entry.identifierValue;\r\n break;\r\n case \"TVDB\":\r\n stateParams.tvdbId = entry.identifierValue;\r\n break;\r\n }\r\n });\r\n\r\n\r\n if (request.query !== \"\") {\r\n stateParams.query = request.query;\r\n }\r\n\r\n if (request.title) {\r\n stateParams.title = request.title;\r\n }\r\n\r\n if (request.categoryName) {\r\n stateParams.category = request.categoryName;\r\n }\r\n\r\n return stateParams;\r\n }\r\n\r\n\r\n}","\r\nSearchHistoryController.$inject = [\"$scope\", \"$state\", \"SearchHistoryService\", \"ConfigService\", \"history\", \"$sce\", \"$filter\", \"$timeout\", \"$http\", \"$uibModal\"];angular\r\n .module('nzbhydraApp')\r\n .controller('SearchHistoryController', SearchHistoryController);\r\n\r\n\r\nfunction SearchHistoryController($scope, $state, SearchHistoryService, ConfigService, history, $sce, $filter, $timeout, $http, $uibModal) {\r\n $scope.limit = 100;\r\n $scope.pagination = {\r\n current: 1\r\n };\r\n var sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n $timeout(function () {\r\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\r\n }, 10);\r\n $scope.filterModel = {};\r\n\r\n //Filter options\r\n $scope.categoriesForFiltering = [];\r\n _.forEach(ConfigService.getSafe().categoriesConfig.categories, function (category) {\r\n $scope.categoriesForFiltering.push({label: category.name, id: category.name})\r\n });\r\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\r\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: 'API'}, {\r\n label: \"Internal\",\r\n value: 'INTERNAL'\r\n }];\r\n\r\n //Preloaded data\r\n $scope.searchRequests = history.searchRequests;\r\n $scope.totalRequests = history.totalRequests;\r\n\r\n var anyUsername = false;\r\n var anyIp = false;\r\n for (var request in $scope.searchRequests) {\r\n if (request.username) {\r\n anyUsername = true;\r\n }\r\n if (request.ip) {\r\n anyIp = true;\r\n }\r\n if (anyIp && anyUsername) {\r\n break;\r\n }\r\n }\r\n $scope.columnSizes = {\r\n time: 10,\r\n query: 30,\r\n category: 10,\r\n additionalParameters: 22,\r\n source: 8,\r\n username: 10,\r\n ip: 10\r\n };\r\n if (ConfigService.getSafe().logging.historyUserInfoType === \"NONE\" || (!anyUsername && !anyIp)) {\r\n $scope.columnSizes.username = 0;\r\n $scope.columnSizes.ip = 0;\r\n $scope.columnSizes.query += 10;\r\n $scope.columnSizes.additionalParameters += 10;\r\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"IP\") {\r\n $scope.columnSizes.username = 0;\r\n $scope.columnSizes.query += 5;\r\n $scope.columnSizes.additionalParameters += 5;\r\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"USERNAME\") {\r\n $scope.columnSizes.ip = 0;\r\n $scope.columnSizes.query += 5;\r\n $scope.columnSizes.additionalParameters += 5;\r\n }\r\n\r\n $scope.update = function () {\r\n SearchHistoryService.getSearchHistory($scope.pagination.current, $scope.limit, $scope.filterModel, sortModel).then(function (history) {\r\n $scope.searchRequests = history.searchRequests;\r\n $scope.totalRequests = history.totalRequests;\r\n });\r\n };\r\n\r\n $scope.$on(\"sort\", function (event, column, sortMode) {\r\n if (sortMode === 0) {\r\n sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n } else {\r\n sortModel = {\r\n column: column,\r\n sortMode: sortMode\r\n };\r\n }\r\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\r\n $scope.update();\r\n });\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\r\n if (filterModel.filterValue) {\r\n $scope.filterModel[column] = filterModel;\r\n } else {\r\n delete $scope.filterModel[column];\r\n }\r\n $scope.update();\r\n });\r\n\r\n\r\n $scope.openSearch = function (request) {\r\n $state.go(\"root.search\", SearchHistoryService.getStateParamsForRepeatedSearch(request), {\r\n inherit: false,\r\n notify: true,\r\n reload: true\r\n });\r\n };\r\n\r\n $scope.formatQuery = function (request) {\r\n if (request.title) {\r\n return request.title;\r\n }\r\n\r\n if (!request.query && request.identifiers.length === 0 && !request.season && !request.episode) {\r\n return \"Update query\";\r\n }\r\n return request.query;\r\n };\r\n\r\n $scope.formatAdditional = function (request) {\r\n var result = [];\r\n if (request.identifiers.length > 0) {\r\n var href;\r\n var key;\r\n var value;\r\n var pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"TMDB\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"TMDB ID\";\r\n href = \"https://www.themoviedb.org/movie/\" + pair.identifierValue;\r\n href = $filter(\"dereferer\")(href);\r\n value = pair.identifierValue;\r\n }\r\n\r\n pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"IMDB\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"IMDB ID\";\r\n href = (\"https://www.imdb.com/title/tt\" + pair.identifierValue).replace(\"tttt\", \"tt\");\r\n href = $filter(\"dereferer\")(href);\r\n value = pair.identifierValue;\r\n }\r\n\r\n pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"TVDB\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"TVDB ID\";\r\n href = \"https://thetvdb.com/?tab=series&id=\" + pair.identifierValue;\r\n href = $filter(\"dereferer\")(href);\r\n value = pair.identifierValue;\r\n }\r\n\r\n pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"TVRAGE\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"TVRage ID\";\r\n href = \"internalapi/redirectRid/\" + pair.identifierValue;\r\n value = pair.identifierValue;\r\n }\r\n\r\n result.push(key + \": \" + '' + value + \"\");\r\n }\r\n if (request.season) {\r\n result.push(\"Season: \" + request.season);\r\n }\r\n if (request.episode) {\r\n result.push(\"Episode: \" + request.episode);\r\n }\r\n if (request.author) {\r\n result.push(\"Author: \" + request.author);\r\n }\r\n return $sce.trustAsHtml(result.join(\", \"));\r\n };\r\n\r\n $scope.showDetails = function (searchId) {\r\n\r\n ModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$http\", \"searchId\"];\r\n function ModalInstanceCtrl($scope, $uibModalInstance, $http, searchId) {\r\n $http.get(\"internalapi/history/searches/details/\" + searchId).then(function (response) {\r\n $scope.details = response.data;\r\n });\r\n }\r\n\r\n $uibModal.open({\r\n templateUrl: 'static/html/search-history-details-modal.html',\r\n controller: ModalInstanceCtrl,\r\n size: \"md\",\r\n resolve: {\r\n searchId: function () {\r\n return searchId;\r\n }\r\n }\r\n });\r\n\r\n\r\n }\r\n\r\n}\r\n\r\n","\r\nSearchController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\", \"$uibModal\", \"$timeout\", \"$sce\", \"growl\", \"SearchService\", \"focus\", \"ConfigService\", \"HydraAuthService\", \"CategoriesService\", \"$element\", \"SearchHistoryService\"];\r\nSearchUpdateModalInstanceCtrl.$inject = [\"$scope\", \"$interval\", \"SearchService\", \"$uibModalInstance\", \"searchRequestId\", \"onCancel\", \"bootstrapped\"];angular\r\n .module('nzbhydraApp')\r\n .controller('SearchController', SearchController);\r\n\r\nfunction SearchController($scope, $http, $stateParams, $state, $uibModal, $timeout, $sce, growl, SearchService, focus, ConfigService, HydraAuthService, CategoriesService, $element, SearchHistoryService) {\r\n\r\n function getNumberOrUndefined(number) {\r\n if (_.isUndefined(number) || _.isNaN(number) || number === \"\") {\r\n return undefined;\r\n }\r\n number = parseInt(number);\r\n if (_.isNumber(number)) {\r\n return number;\r\n } else {\r\n return undefined;\r\n }\r\n }\r\n\r\n var searchRequestId = 0;\r\n var isSearchCancelled = false;\r\n var epochEnter;\r\n\r\n //Fill the form with the search values we got from the state params (so that their values are the same as in the current url)\r\n $scope.mode = $stateParams.mode;\r\n $scope.query = \"\";\r\n $scope.selectedItem = null;\r\n $scope.categories = _.filter(CategoriesService.getAllCategories(), function (c) {\r\n return c.mayBeSelected && !(c.ignoreResultsFrom === \"INTERNAL\" || c.ignoreResultsFrom === \"BOTH\");\r\n });\r\n $scope.minsize = getNumberOrUndefined($stateParams.minsize);\r\n $scope.maxsize = getNumberOrUndefined($stateParams.maxsize);\r\n if (angular.isDefined($stateParams.category) && $stateParams.category) {\r\n $scope.category = CategoriesService.getByName($stateParams.category);\r\n } else {\r\n $scope.category = CategoriesService.getDefault();\r\n $scope.minsize = $scope.category.minSizePreset;\r\n $scope.maxsize = $scope.category.maxSizePreset;\r\n }\r\n $scope.category = _.isNullOrEmpty($stateParams.category) ? CategoriesService.getDefault() : CategoriesService.getByName($stateParams.category);\r\n $scope.season = $stateParams.season;\r\n $scope.episode = $stateParams.episode;\r\n $scope.query = $stateParams.query;\r\n\r\n $scope.minage = getNumberOrUndefined($stateParams.minage);\r\n $scope.maxage = getNumberOrUndefined($stateParams.maxage);\r\n if (angular.isDefined($stateParams.indexers)) {\r\n $scope.indexers = decodeURIComponent($stateParams.indexers).split(\",\");\r\n }\r\n if (angular.isDefined($stateParams.title) || (angular.isDefined($stateParams.tmdbId) || angular.isDefined($stateParams.imdbId) || angular.isDefined($stateParams.tvmazeId) || angular.isDefined($stateParams.rid) || angular.isDefined($stateParams.tvdbId))) {\r\n var width = calculateWidth($stateParams.title) + 30;\r\n $scope.selectedItemWidth = width + \"px\";\r\n $scope.selectedItem = {\r\n tmdbId: $stateParams.tmdbId,\r\n imdbId: $stateParams.imdbId,\r\n tvmazeId: $stateParams.tvmazeId,\r\n rid: $stateParams.rid,\r\n tvdbId: $stateParams.tvdbId,\r\n title: $stateParams.title\r\n }\r\n }\r\n\r\n $scope.showIndexers = {};\r\n\r\n $scope.searchHistory = [];\r\n\r\n var safeConfig = ConfigService.getSafe();\r\n $scope.showIndexerSelection = HydraAuthService.getUserInfos().showIndexerSelection;\r\n\r\n\r\n $scope.typeAheadWait = 300;\r\n\r\n $scope.autocompleteLoading = false;\r\n $scope.isAskById = $scope.category.searchType === \"TVSEARCH\" || $scope.category.searchType === \"MOVIE\";\r\n $scope.isById = {value: $scope.selectedItem !== null || angular.isUndefined($scope.mode) || $scope.mode === null}; //If true the user wants to search by id so we enable autosearch. Was unable to achieve this using a simple boolean. Set to false if last search was not by ID\r\n $scope.availableIndexers = [];\r\n $scope.selectedIndexers = [];\r\n $scope.autocompleteClass = \"autocompletePosterMovies\";\r\n\r\n $scope.toggleCategory = function (searchCategory) {\r\n var oldCategory = $scope.category;\r\n $scope.category = searchCategory;\r\n\r\n //Show checkbox to ask if the user wants to search by ID (using autocomplete)\r\n if ($scope.category.searchType === \"TVSEARCH\" || $scope.category.searchType === \"MOVIE\") {\r\n $scope.isAskById = true;\r\n $scope.isById.value = true;\r\n } else {\r\n $scope.isAskById = false;\r\n $scope.isById.value = false;\r\n }\r\n\r\n if (oldCategory.searchType !== searchCategory.searchType) {\r\n $scope.selectedItem = null;\r\n }\r\n\r\n focus('searchfield');\r\n\r\n //Hacky way of triggering the autocomplete loading\r\n var searchModel = $element.find(\"#searchfield\").controller(\"ngModel\");\r\n if (angular.isDefined(searchModel.$viewValue)) {\r\n searchModel.$setViewValue(searchModel.$viewValue + \" \");\r\n }\r\n\r\n if (safeConfig.categoriesConfig.enableCategorySizes) {\r\n var min = searchCategory.minSizePreset;\r\n var max = searchCategory.maxSizePreset;\r\n if (_.isNumber(min)) {\r\n $scope.minsize = min;\r\n } else {\r\n $scope.minsize = \"\";\r\n }\r\n if (_.isNumber(max)) {\r\n $scope.maxsize = max;\r\n } else {\r\n $scope.maxsize = \"\";\r\n }\r\n }\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n };\r\n\r\n // Any function returning a promise object can be used to load values asynchronously\r\n $scope.getAutocomplete = function (val) {\r\n $scope.autocompleteLoading = true;\r\n //Expected model returned from API:\r\n //label: What to show in the results\r\n //title: Will be used for file search\r\n //value: Will be used as extraInfo (ttid oder tvdb id)\r\n //poster: url of poster to show\r\n\r\n //Don't use autocomplete if checkbox is disabled\r\n if (!$scope.isById.value || $scope.selectedItem) {\r\n return {};\r\n }\r\n\r\n if ($scope.category.searchType === \"MOVIE\") {\r\n return $http.get('internalapi/autocomplete/MOVIE/', {params: {input: val}}).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data;\r\n });\r\n } else if ($scope.category.searchType === \"TVSEARCH\") {\r\n return $http.get('internalapi/autocomplete/TV/', {params: {input: val}}).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data;\r\n });\r\n } else {\r\n return {};\r\n }\r\n };\r\n\r\n $scope.onTypeAheadEnter = function () {\r\n if (angular.isDefined(epochEnter)) {\r\n //Very hacky way of preventing a press of \"enter\" to select an autocomplete item from triggering a search\r\n //This is called *after* selectAutoComplete() is called\r\n var epochEnterNow = (new Date).getTime();\r\n var diff = epochEnterNow - epochEnter;\r\n if (diff > 50) {\r\n $scope.initiateSearch();\r\n }\r\n } else {\r\n $scope.initiateSearch();\r\n }\r\n };\r\n\r\n $scope.onTypeAheadKeyDown = function (event) {\r\n if (event.keyCode === 8) {\r\n if ($scope.query === \"\") {\r\n $scope.clearAutocomplete();\r\n }\r\n }\r\n };\r\n\r\n $scope.onDropOnQueryInput = function (event) {\r\n if ($scope.searchHistoryDragged === null || $scope.searchHistoryDragged === undefined) {\r\n return;\r\n }\r\n\r\n $scope.category = CategoriesService.getByName($scope.searchHistoryDragged.categoryName);\r\n $scope.season = $scope.searchHistoryDragged.season;\r\n $scope.episode = $scope.searchHistoryDragged.episode;\r\n $scope.query = $scope.searchHistoryDragged.query;\r\n\r\n if ($scope.searchHistoryDragged.title != null) {\r\n var width = calculateWidth($scope.searchHistoryDragged.title) + 30;\r\n $scope.selectedItemWidth = width + \"px\";\r\n }\r\n\r\n var tvmaze = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"TVMAZE\"});\r\n var tmdb = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"TMDB\"});\r\n var imdb = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"IMDB\"});\r\n var tvdb = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"TVDB\"});\r\n $scope.selectedItem = {\r\n tmdbId: tmdb === undefined ? null : tmdb.identifierValue,\r\n imdbId: imdb === undefined ? null : imdb.identifierValue,\r\n tvmazeId: tvmaze === undefined ? null : tvmaze.identifierValue,\r\n tvdbId: tvdb === undefined ? null : tvdb.identifierValue,\r\n title: $scope.searchHistoryDragged.title\r\n }\r\n\r\n event.preventDefault();\r\n\r\n $scope.searchHistoryDragged = null;\r\n focus('searchfield');\r\n $scope.status.isopen = false;\r\n }\r\n\r\n $scope.$on(\"searchHistoryDrag\", function (event, data) {\r\n $scope.searchHistoryDragged = JSON.parse(data);\r\n })\r\n\r\n //Is called when the search page is opened with params, either because the user initiated the search (which triggered a goTo to this page) or because a search URL was entered\r\n $scope.startSearch = function () {\r\n isSearchCancelled = false;\r\n searchRequestId = Math.round(Math.random() * 99999);\r\n var modalInstance = $scope.openModal(searchRequestId);\r\n\r\n var indexers = angular.isUndefined($scope.indexers) ? undefined : $scope.indexers.join(\",\");\r\n SearchService.search(searchRequestId, $scope.category.name, $scope.query, $scope.selectedItem, $scope.season, $scope.episode, $scope.minsize, $scope.maxsize, $scope.minage, $scope.maxage, indexers, $scope.mode).then(function () {\r\n //modalInstance.close();\r\n SearchService.setModalInstance(modalInstance);\r\n if (!isSearchCancelled) {\r\n $state.go(\"root.search.results\", {\r\n minsize: $scope.minsize,\r\n maxsize: $scope.maxsize,\r\n minage: $scope.minage,\r\n maxage: $scope.maxage\r\n }, {\r\n inherit: true\r\n });\r\n }\r\n },\r\n function () {\r\n modalInstance.close();\r\n });\r\n };\r\n\r\n $scope.openModal = function openModal(searchRequestId) {\r\n return $uibModal.open({\r\n templateUrl: 'static/html/search-state.html',\r\n controller: SearchUpdateModalInstanceCtrl,\r\n size: \"md\",\r\n backdrop: \"static\",\r\n backdropClass: \"waiting-cursor\",\r\n resolve: {\r\n searchRequestId: function () {\r\n return searchRequestId;\r\n },\r\n onCancel: function () {\r\n function cancel() {\r\n isSearchCancelled = true;\r\n }\r\n\r\n return cancel;\r\n }\r\n }\r\n });\r\n };\r\n\r\n $scope.goToSearchUrl = function () {\r\n //State params (query parameters) should all be lowercase\r\n var stateParams = {};\r\n stateParams.mode = $scope.category.searchType.toLowerCase();\r\n stateParams.imdbId = $scope.selectedItem === null ? null : $scope.selectedItem.imdbId;\r\n stateParams.tmdbId = $scope.selectedItem === null ? null : $scope.selectedItem.tmdbId;\r\n stateParams.tvdbId = $scope.selectedItem === null ? null : $scope.selectedItem.tvdbId;\r\n stateParams.tvrageId = $scope.selectedItem === null ? null : $scope.selectedItem.tvrageId;\r\n stateParams.tvmazeId = $scope.selectedItem === null ? null : $scope.selectedItem.tvmazeId;\r\n stateParams.title = $scope.selectedItem === null ? null : $scope.selectedItem.title;\r\n stateParams.season = $scope.season;\r\n stateParams.episode = $scope.episode;\r\n stateParams.query = $scope.query;\r\n stateParams.minsize = $scope.minsize;\r\n stateParams.maxsize = $scope.maxsize;\r\n stateParams.minage = $scope.minage;\r\n stateParams.maxage = $scope.maxage;\r\n stateParams.category = $scope.category.name;\r\n stateParams.indexers = encodeURIComponent($scope.selectedIndexers.join(\",\"));\r\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.repeatSearch = function (request) {\r\n var stateParams = SearchHistoryService.getStateParamsForRepeatedSearch(request);\r\n stateParams.indexers = encodeURIComponent($scope.selectedIndexers.join(\",\"));\r\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.searchBoxTooltip = \"Prefix terms with -- to exclude'\";\r\n $scope.$watchGroup(['isAskById', 'selectedItem'], function () {\r\n if (!$scope.isAskById) {\r\n $scope.searchBoxTooltip = \"Prefix terms with -- to exclude\";\r\n } else if ($scope.selectedItem === null) {\r\n $scope.searchBoxTooltip = \"Enter search terms for autocomplete\";\r\n } else {\r\n $scope.searchBoxTooltip = \"Enter additional search terms to limit the query\";\r\n }\r\n });\r\n\r\n $scope.clearAutocomplete = function () {\r\n $scope.selectedItem = null;\r\n $scope.query = \"\"; //Input is now for autocomplete and not for limiting the results\r\n focus('searchfield');\r\n };\r\n\r\n $scope.clearQuery = function () {\r\n $scope.selectedItem = null;\r\n $scope.query = \"\";\r\n focus('searchfield');\r\n };\r\n\r\n function calculateWidth(text) {\r\n var canvas = calculateWidth.canvas || (calculateWidth.canvas = document.createElement(\"canvas\"));\r\n var context = canvas.getContext(\"2d\");\r\n context.font = \"13px Roboto\";\r\n return context.measureText(text).width;\r\n }\r\n\r\n $scope.selectAutocompleteItem = function ($item) {\r\n $scope.selectedItem = $item;\r\n $scope.query = \"\";\r\n epochEnter = (new Date).getTime();\r\n var width = calculateWidth($item.title) + 30;\r\n $scope.selectedItemWidth = width + \"px\";\r\n };\r\n\r\n $scope.initiateSearch = function () {\r\n if ($scope.selectedIndexers.length === 0) {\r\n growl.error(\"You didn't select any indexers\");\r\n return;\r\n }\r\n if ($scope.selectedItem) {\r\n //Movie or tv show was selected\r\n $scope.goToSearchUrl();\r\n } else {\r\n //Simple query search\r\n $scope.goToSearchUrl();\r\n }\r\n };\r\n\r\n $scope.autocompleteActive = function () {\r\n return $scope.isAskById;\r\n };\r\n\r\n $scope.seriesSelected = function () {\r\n return $scope.category.searchType === \"TVSEARCH\";\r\n };\r\n\r\n $scope.toggleIndexer = function (indexer) {\r\n $scope.availableIndexers[indexer.name].activated = !$scope.availableIndexers[indexer.name].activated;\r\n };\r\n\r\n function isIndexerPreselected(indexer) {\r\n if (angular.isUndefined($scope.indexers)) {\r\n return indexer.preselect;\r\n } else {\r\n return _.contains($scope.indexers, indexer.name);\r\n }\r\n }\r\n\r\n function getAvailableIndexers() {\r\n var alreadySelected = $scope.selectedIndexers;\r\n var previouslyAvailable = _.pluck($scope.availableIndexers, \"name\");\r\n $scope.selectedIndexers = [];\r\n var availableIndexersList = _.chain(safeConfig.indexers).filter(function (indexer) {\r\n if (!indexer.showOnSearch) {\r\n return false;\r\n }\r\n var categorySelectedForIndexer = (angular.isUndefined(indexer.categories) || indexer.categories.length === 0 || $scope.category.name.toLowerCase() === \"all\" || indexer.categories.indexOf($scope.category.name) > -1);\r\n return categorySelectedForIndexer;\r\n }).sortBy(function (indexer) {\r\n return indexer.name.toLowerCase();\r\n })\r\n .map(function (indexer) {\r\n return {\r\n name: indexer.name,\r\n activated: isIndexerPreselected(indexer),\r\n preselect: indexer.preselect,\r\n categories: indexer.categories,\r\n searchModuleType: indexer.searchModuleType\r\n };\r\n }).value();\r\n _.forEach(availableIndexersList, function (x) {\r\n var deselectedBefore = (_.indexOf(previouslyAvailable, x.name) > -1 && _.indexOf(alreadySelected, x.name) === -1);\r\n var selectedBefore = (_.indexOf(previouslyAvailable, x.name) > -1 && _.indexOf(alreadySelected, x.name) > -1);\r\n if ((x.activated && !deselectedBefore) || selectedBefore) {\r\n $scope.selectedIndexers.push(x.name);\r\n }\r\n });\r\n return availableIndexersList;\r\n }\r\n\r\n\r\n $scope.formatRequest = function (request) {\r\n return $sce.trustAsHtml(SearchHistoryService.formatRequest(request, false, true, true, true));\r\n };\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n\r\n function getAndSetSearchRequests() {\r\n SearchHistoryService.getSearchHistoryForSearching().then(function (response) {\r\n $scope.searchHistory = response.searchRequests;\r\n });\r\n }\r\n\r\n if ($scope.mode) {\r\n $scope.startSearch();\r\n } else {\r\n //Getting the search history only makes sense when we're not currently searching\r\n _.defer(getAndSetSearchRequests);\r\n }\r\n\r\n $scope.$on(\"searchResultsShown\", function () {\r\n _.defer(getAndSetSearchRequests); //Defer because otherwise the results are only shown when this returns which may take a while with big databases\r\n });\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('SearchUpdateModalInstanceCtrl', SearchUpdateModalInstanceCtrl);\r\n\r\nfunction SearchUpdateModalInstanceCtrl($scope, $interval, SearchService, $uibModalInstance, searchRequestId, onCancel, bootstrapped) {\r\n\r\n var loggedSearchFinished = false;\r\n $scope.messages = [];\r\n $scope.indexerSelectionFinished = false;\r\n $scope.indexersSelected = 0;\r\n $scope.indexersFinished = 0;\r\n\r\n var socket = new SockJS(bootstrapped.baseUrl + 'websocket');\r\n var stompClient = Stomp.over(socket);\r\n stompClient.debug = null;\r\n stompClient.connect({}, function (frame) {\r\n stompClient.subscribe('/topic/searchState', function (message) {\r\n var data = JSON.parse(message.body);\r\n if (searchRequestId !== data.searchRequestId) {\r\n return;\r\n }\r\n $scope.searchFinished = data.searchFinished;\r\n $scope.indexersSelected = data.indexersSelected;\r\n $scope.indexersFinished = data.indexersFinished;\r\n $scope.progressMax = data.indexersSelected;\r\n if ($scope.progressMax > data.indexersSelected) {\r\n $scope.progressMax = \">=\" + data.indexersSelected;\r\n }\r\n if (data.messages) {\r\n $scope.messages = data.messages;\r\n }\r\n if ($scope.searchFinished && !loggedSearchFinished) {\r\n $scope.messages.push(\"Finished searching. Preparing results...\");\r\n loggedSearchFinished = true;\r\n }\r\n });\r\n });\r\n\r\n $scope.cancelSearch = function () {\r\n onCancel();\r\n $uibModalInstance.dismiss();\r\n };\r\n\r\n $scope.hasResults = function (message) {\r\n return /^[^0]\\d+.*/.test(message);\r\n };\r\n\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive('draggable', ['$rootScope', function ($rootScope) {\r\n return {\r\n restrict: 'A',\r\n link: function (scope, el, attrs, controller) {\r\n\r\n el.bind(\"dragstart\", function (e) {\r\n $rootScope.$emit(\"searchHistoryDrag\", el.attr(\"data-request\"));\r\n $rootScope.$broadcast(\"searchHistoryDrag\", el.attr(\"data-request\"));\r\n });\r\n }\r\n }\r\n}]);\r\n\r\n","\r\nRestartService.$inject = [\"growl\", \"NzbHydraControlService\", \"$uibModal\"];\r\nRestartModalInstanceCtrl.$inject = [\"$scope\", \"$timeout\", \"$http\", \"$window\", \"RequestsErrorHandler\", \"message\", \"baseUrl\"];angular\r\n .module('nzbhydraApp')\r\n .factory('RestartService', RestartService);\r\n\r\nfunction RestartService(growl, NzbHydraControlService, $uibModal) {\r\n\r\n return {\r\n restart: restart,\r\n startCountdown: startCountdown\r\n };\r\n\r\n function restart(message) {\r\n NzbHydraControlService.restart().then(function (response) {\r\n startCountdown(message, response.data.message);\r\n }, function () {\r\n growl.info(\"Unable to send restart command.\");\r\n })\r\n }\r\n\r\n function startCountdown(message, baseUrl) {\r\n $uibModal.open({\r\n templateUrl: 'static/html/restart-modal.html',\r\n controller: RestartModalInstanceCtrl,\r\n size: \"md\",\r\n backdrop: 'static',\r\n keyboard: false,\r\n resolve: {\r\n message: function () {\r\n return message;\r\n },\r\n baseUrl: function () {\r\n return baseUrl;\r\n }\r\n }\r\n });\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('RestartModalInstanceCtrl', RestartModalInstanceCtrl);\r\n\r\nfunction RestartModalInstanceCtrl($scope, $timeout, $http, $window, RequestsErrorHandler, message, baseUrl) {\r\n\r\n message = (angular.isDefined(message) ? message : \"\");\r\n $scope.message = message + \"Will reload page when NZBHydra is back\";\r\n $scope.baseUrl = baseUrl;\r\n $scope.pingUrl = angular.isDefined(baseUrl) ? (baseUrl + \"/internalapi/control/ping\") : \"internalapi/control/ping\";\r\n\r\n $scope.internalCaR = function (message, timer) {\r\n if (timer === 45) {\r\n $scope.message = message + \" Restarting takes longer than expected. You might want to check the log to see what's going on.\";\r\n } else {\r\n $scope.message = message + \" Will reload page when NZBHydra is back.\";\r\n $timeout(function () {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n $http.get($scope.pingUrl, {ignoreLoadingBar: true}).then(\r\n function () {\r\n $timeout(function () {\r\n $scope.message = \"Reloading page...\";\r\n if (angular.isDefined($scope.baseUrl)) {\r\n $window.location.href = $scope.baseUrl;\r\n } else {\r\n $window.location.reload();\r\n }\r\n }, 2000); //Give Hydra some time to load in the background, it might return the ping but not be completely up yet\r\n }, function () {\r\n $scope.internalCaR(message, timer + 1);\r\n });\r\n });\r\n }, 1000);\r\n $scope.message = message + \" Will reload page when NZBHydra is back.\";\r\n }\r\n };\r\n\r\n //Wait three seconds because otherwise the currently running instance will be found\r\n $timeout(function () {\r\n $scope.internalCaR(message, 0);\r\n }, 3000)\r\n}","\r\nNzbHydraControlService.$inject = [\"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('NzbHydraControlService', NzbHydraControlService);\r\n\r\nfunction NzbHydraControlService($http) {\r\n\r\n return {\r\n restart: restart,\r\n shutdown: shutdown\r\n };\r\n\r\n function restart() {\r\n return $http.get(\"internalapi/control/restart\");\r\n }\r\n\r\n function shutdown() {\r\n return $http.get(\"internalapi/control/shutdown\");\r\n }\r\n\r\n}\r\n","\r\nNzbDownloadService.$inject = [\"$http\", \"ConfigService\", \"DownloaderCategoriesService\"];angular\r\n .module('nzbhydraApp')\r\n .factory('NzbDownloadService', NzbDownloadService);\r\n\r\nfunction NzbDownloadService($http, ConfigService, DownloaderCategoriesService) {\r\n\r\n var service = {\r\n download: download,\r\n getEnabledDownloaders: getEnabledDownloaders\r\n };\r\n\r\n return service;\r\n\r\n function sendNzbAddCommand(downloader, searchResults, category) {\r\n var params = {\r\n downloaderName: downloader.name,\r\n searchResults: searchResults,\r\n category: category\r\n };\r\n return $http.put(\"internalapi/downloader/addNzbs\", params);\r\n }\r\n\r\n function download(downloader, searchResults, alwaysAsk) {\r\n var category = downloader.defaultCategory;\r\n if (alwaysAsk || (_.isNullOrEmpty(category) && category !== \"Use original category\") && category !== \"Use mapped category\" && category !== \"Use no category\") {\r\n return DownloaderCategoriesService.openCategorySelection(downloader).then(function (category) {\r\n return sendNzbAddCommand(downloader, searchResults, category);\r\n }, function (result) {\r\n return result;\r\n });\r\n } else {\r\n return sendNzbAddCommand(downloader, searchResults, category)\r\n }\r\n }\r\n\r\n function getEnabledDownloaders() {\r\n return _.filter(ConfigService.getSafe().downloading.downloaders, \"enabled\");\r\n }\r\n}\r\n\r\n","\nNotificationService.$inject = [\"$http\"];angular\n .module('nzbhydraApp')\n .service('NotificationService', NotificationService);\n\nfunction NotificationService($http) {\n\n var eventTypesData = {\n AUTH_FAILURE: {\n readable: \"Auth failure\",\n titleTemplate: \"Auth failure\",\n bodyTemplate: \"NZBHydra: A login for username $username$ failed. IP: $ip$.\",\n templateHelp: \"Available variables: $username$, $ip$.\",\n messageType: \"FAILURE\"\n },\n RESULT_DOWNLOAD: {\n readable: \"NZB download\",\n titleTemplate: \"NZB download\",\n bodyTemplate: \"NZBHydra: The result \\\"$title$\\\" was grabbed from indexer $indexerName$.\",\n templateHelp: \"Available variables: $title, $indexerName$, $source$ (NZB or torrent), $age$ ([] for torrents).\",\n messageType: \"INFO\"\n },\n RESULT_DOWNLOAD_COMPLETION: {\n readable: \"Download completion\",\n titleTemplate: \"Download completion\",\n bodyTemplate: \"NZBHydra: Download of \\\"$title$\\\" has finished. Download result: $downloadResult$.\",\n templateHelp: \"Requires the downloading tool to be configured. Available variables: $title, $downloadResult$.\",\n messageType: \"INFO\"\n },\n INDEXER_DISABLED: {\n readable: \"Indexer disabled\",\n titleTemplate: \"Indexer disabled\",\n bodyTemplate: \"NZBHydra: Indexer $indexerName$ was disabled (state: $state$). Message:\\n$message$.\",\n templateHelp: \"Available variables: $indexerName$, $state$, $message$.\",\n messageType: \"WARNING\"\n },\n INDEXER_REENABLED: {\n readable: \"Indexer reenabled after error\",\n titleTemplate: \"Indexer reenabled after error\",\n bodyTemplate: \"NZBHydra: Indexer $indexerName$ was reenabled after a previous error. It had been disabled since $disabledAt$.\",\n templateHelp: \"Available variables: $indexerName$, $disabledAt$.\",\n messageType: \"SUCCESS\"\n },\n UPDATE_INSTALLED: {\n readable: \"Automatic update installed\",\n titleTemplate: \"Update installed\",\n bodyTemplate: \"NZBHydra: A new version of was installed: $version$\",\n templateHelp: \"Available variables: $version$.\",\n messageType: \"SUCCESS\"\n },\n VIP_RENEWAL_REQUIRED: {\n readable: \"VIP renewal required (14 day warning)\",\n titleTemplate: \"VIP renewal required\",\n bodyTemplate: \"NZBHydra: VIP access for indexer $indexerName$ will run out soon: $expirationDate$.\",\n templateHelp: \"Available variables: $indexerName$, $expirationDate$.\",\n messageType: \"WARNING\"\n }\n }\n\n this.getAllEventTypes = function () {\n return _.keys(eventTypesData);\n };\n\n this.getAllData = function () {\n return eventTypesData;\n };\n\n this.humanize = function (eventType) {\n return eventTypesData[eventType].readable;\n };\n\n this.getTemplateHelp = function (eventType) {\n return eventTypesData[eventType].templateHelp;\n };\n\n this.getTitleTemplate = function (eventType) {\n return eventTypesData[eventType].titleTemplate;\n };\n\n this.getBodyTemplate = function (eventType) {\n return eventTypesData[eventType].bodyTemplate;\n };\n\n this.testNotification = function (eventType) {\n return $http.get('internalapi/notifications/test/' + eventType);\n }\n\n\n}","\nNotificationHistoryController.$inject = [\"$scope\", \"StatsService\", \"preloadData\", \"ConfigService\", \"$timeout\", \"NotificationService\"];angular\n .module('nzbhydraApp')\n .controller('NotificationHistoryController', NotificationHistoryController);\n\n\nfunction NotificationHistoryController($scope, StatsService, preloadData, ConfigService, $timeout, NotificationService) {\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n var sortModel = {\n column: \"time\",\n sortMode: 2\n };\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n }, 10);\n $scope.filterModel = {};\n\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\n\n\n //Preloaded data\n $scope.notifications = preloadData.notifications;\n $scope.totalNotifications = preloadData.totalNotifications;\n\n\n $scope.columnSizes = {\n time: 10,\n type: 15,\n title: 15,\n body: 40,\n urls: 20\n };\n\n $scope.update = function () {\n StatsService.getNotificationHistory($scope.pagination.current, $scope.limit, $scope.filterModel, sortModel).then(function (data) {\n $scope.notifications = data.notifications;\n $scope.totalNotifications = data.totalNotifications;\n });\n };\n\n\n $scope.eventTypesForFiltering = [];\n var eventTypes = NotificationService.getAllEventTypes();\n _.each(eventTypes, function (key) {\n $scope.eventTypesForFiltering.push({label: NotificationService.humanize(key), id: key})\n })\n\n $scope.$on(\"sort\", function (event, column, sortMode) {\n if (sortMode === 0) {\n column = \"time\";\n sortMode = 2;\n }\n sortModel = {\n column: column,\n sortMode: sortMode\n };\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n $scope.update();\n });\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filterValue) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n $scope.update();\n })\n\n $scope.formatEventType = function (notification) {\n return NotificationService.humanize(notification.notificationEventType);\n };\n\n $scope.formatEventBody = function (notification) {\n return notification.body.replace(\"\\n\", \"
          \");\n };\n\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateEpoch', reformatDateEpoch);\n\nfunction reformatDateEpoch() {\n return function (date) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm\");\n\n }\n}\n","\r\nModalService.$inject = [\"$uibModal\"];\r\nModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"headline\", \"message\", \"params\", \"textAlign\"];angular\r\n .module('nzbhydraApp')\r\n .factory('ModalService', ModalService);\r\n\r\nfunction ModalService($uibModal) {\r\n\r\n return {\r\n open: open\r\n };\r\n\r\n function open(headline, message, params, size, textAlign) {\r\n //params example:\r\n /*\r\n var p =\r\n {\r\n yes: {\r\n text: \"Yes\", //default: Ok\r\n onYes: function() {}\r\n },\r\n no: { //default: Empty\r\n text: \"No\",\r\n onNo: function () {\r\n }\r\n },\r\n cancel: {\r\n text: \"Cancel\", //default: Cancel\r\n onCancel: function () {\r\n }\r\n }\r\n };\r\n */\r\n if (angular.isUndefined(textAlign)) {\r\n textAlign = \"center\";\r\n }\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'static/html/modal.html',\r\n controller: 'ModalInstanceCtrl',\r\n size: angular.isDefined(size) ? size : \"md\",\r\n resolve: {\r\n headline: function () {\r\n return headline;\r\n },\r\n message: function () {\r\n return message;\r\n },\r\n params: function () {\r\n return params;\r\n },\r\n textAlign: function () {\r\n return textAlign;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then(function () {\r\n\r\n }, function () {\r\n\r\n });\r\n }\r\n\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('ModalInstanceCtrl', ModalInstanceCtrl);\r\n\r\nfunction ModalInstanceCtrl($scope, $uibModalInstance, headline, message, params, textAlign) {\r\n\r\n $scope.message = message;\r\n $scope.headline = headline;\r\n $scope.params = params;\r\n $scope.showCancel = angular.isDefined(params) && angular.isDefined(params.cancel);\r\n $scope.showNo = angular.isDefined(params) && angular.isDefined(params.no);\r\n $scope.textAlign = textAlign;\r\n\r\n if (angular.isUndefined(params) || angular.isUndefined(params.yes)) {\r\n $scope.params = {\r\n yes: {\r\n text: \"Ok\"\r\n }\r\n }\r\n } else if (angular.isUndefined(params.yes.text)) {\r\n params.yes.text = \"Yes\";\r\n }\r\n\r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isUndefined($scope.params.no.text)) {\r\n $scope.params.no.text = \"No\";\r\n }\r\n\r\n if (angular.isDefined(params) && angular.isDefined(params.cancel) && angular.isUndefined($scope.params.cancel.text)) {\r\n $scope.params.cancel.text = \"Cancel\";\r\n }\r\n\r\n $scope.yes = function () {\r\n $uibModalInstance.close();\r\n if (angular.isDefined(params) && angular.isDefined(params.yes) && angular.isDefined($scope.params.yes.onYes)) {\r\n $scope.params.yes.onYes();\r\n }\r\n };\r\n\r\n $scope.no = function () {\r\n $uibModalInstance.close();\r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isDefined($scope.params.no.onNo)) {\r\n $scope.params.no.onNo($uibModalInstance);\r\n }\r\n };\r\n\r\n $scope.cancel = function () {\r\n $uibModalInstance.dismiss();\r\n if (angular.isDefined(params.cancel) && angular.isDefined($scope.params.cancel.onCancel)) {\r\n $scope.params.cancel.onCancel();\r\n }\r\n };\r\n\r\n $scope.$on(\"modal.closing\", function (targetScope, reason, c) {\r\n if (reason == \"backdrop click\") {\r\n $scope.cancel();\r\n }\r\n });\r\n}\r\n","angular\n .module('nzbhydraApp')\n .service('GeneralModalService', GeneralModalService);\n\nfunction GeneralModalService() {\n\n\n this.open = function (msg, template, templateUrl, size, data) {\n\n //Prevent circular dependency\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\n var $uibModal = myInjector.get(\"$uibModal\");\n var params = {};\n\n if (angular.isUndefined(size)) {\n params[\"size\"] = size;\n }\n if (angular.isUndefined(template)) {\n if (angular.isUndefined(templateUrl)) {\n params[\"template\"] = '
          ' + msg + '
          ';\n } else {\n params[\"templateUrl\"] = templateUrl;\n }\n } else {\n params[\"template\"] = template;\n }\n params[\"resolve\"] =\n {\n data: function () {\n return data;\n }\n };\n\n var modalInstance = $uibModal.open(params);\n\n modalInstance.result.then();\n\n };\n\n\n}","\nMigrationService.$inject = [\"$uibModal\"];\nMigrationModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$interval\", \"$http\", \"blockUI\", \"ModalService\"];angular\n .module('nzbhydraApp')\n .factory('MigrationService', MigrationService);\n\nfunction MigrationService($uibModal) {\n\n return {\n migrate: migrate\n };\n\n function migrate() {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/migration-modal.html',\n controller: 'MigrationModalInstanceCtrl',\n size: \"md\",\n backdrop: 'static',\n keyboard: false\n });\n\n modalInstance.result.then(function () {\n ConfigService.reloadConfig();\n }, function () {\n });\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('MigrationModalInstanceCtrl', MigrationModalInstanceCtrl);\n\nfunction MigrationModalInstanceCtrl($scope, $uibModalInstance, $interval, $http, blockUI, ModalService) {\n\n $scope.baseUrl = \"http://127.0.0.1:5075\";\n\n $scope.foo = {isMigrating: false, baseUrl: $scope.baseUrl};\n $scope.doMigrateDatabase = true;\n\n $scope.yes = function () {\n var params;\n var url;\n if ($scope.foo.baseUrl && $scope.foo.isFileBasedOpen) {\n $scope.foo.baseUrl = null;\n }\n\n\n if ($scope.foo.isUrlBasedOpen) {\n url = \"internalapi/migration/url\";\n params = {baseurl: $scope.foo.baseUrl, doMigrateDatabase: $scope.doMigrateDatabase};\n if (!params.baseurl) {\n $scope.foo.isMigrating = false;\n ModalService.open(\"Requirements not met\", \"You did not enter a URL\", {\n yes: {\n text: \"OK\"\n }\n });\n return;\n }\n } else {\n url = \"internalapi/migration/files\";\n params = {\n settingsCfgFile: $scope.foo.settingsCfgFile,\n dbFile: $scope.foo.nzbhydraDbFile,\n doMigrateDatabase: $scope.doMigrateDatabase\n };\n if (!params.settingsCfgFile || (!params.dbFile && params.doMigrateDatabase)) {\n $scope.foo.isMigrating = false;\n ModalService.open(\"Requirements not met\", \"You did not enter all required valued\", {\n yes: {\n text: \"OK\"\n }\n });\n return;\n }\n }\n\n $scope.foo.isMigrating = true;\n\n var updateMigrationMessagesInterval = $interval(function () {\n $http.get(\"internalapi/migration/messages\").then(function (response) {\n $scope.foo.messages = response.data;\n },\n function () {\n $interval.cancel(updateMigrationMessagesInterval);\n $scope.foo.isMigrating = false;\n }\n );\n }, 500);\n\n $http.get(url, {params: params}).then(function (response) {\n var message;\n blockUI.stop();\n var data = response.data;\n if (!data.requirementsMet) {\n $interval.cancel(updateMigrationMessagesInterval);\n $scope.foo.isMigrating = false;\n ModalService.open(\"Requirements not met\", \"An error occurred while preparing the migration:
          \" + data.error, {\n yes: {\n text: \"OK\"\n }\n });\n } else if (!data.configMigrated) {\n $interval.cancel(updateMigrationMessagesInterval);\n $uibModalInstance.dismiss();\n $scope.foo.isMigrating = false;\n ModalService.open(\"Config migration failed\", \"An error occurred while migrating the config. Migration failed:
          \" + data.error, {\n yes: {\n text: \"OK\"\n }\n });\n } else if (!data.databaseMigrated) {\n $interval.cancel(updateMigrationMessagesInterval);\n $uibModalInstance.dismiss();\n $scope.foo.isMigrating = false;\n message = \"An error occurred while migrating the database.
          \" + data.error + \"
          . The config was migrated successfully though.\";\n if (data.messages.length > 0) {\n message += '

          The following warnings resulted from the config migration:
            ';\n _.forEach(data.messages, function (msg) {\n message += \"
          • \" + msg + \"
          • \";\n });\n message += \"
          \";\n }\n ModalService.open(\"Database migration failed\", message, {\n yes: {\n text: \"OK\"\n }\n });\n } else {\n $interval.cancel(updateMigrationMessagesInterval);\n $uibModalInstance.dismiss();\n $scope.foo.isMigrating = false;\n message = \"The migration was completed successfully.\";\n if (data.warningMessages.length > 0) {\n message += '

          The following warnings resulted from the config migration:
            ';\n _.forEach(data.warningMessages, function (msg) {\n message += \"
          • \" + msg + \"
          • \";\n });\n message += \"
          \";\n }\n message += \"

          NZBHydra needs to restart for the changes to be effective.\";\n ModalService.open(\"Migration successful\", message, {\n yes: {\n onYes: function () {\n RestartService.restart();\n },\n text: \"Restart\"\n },\n cancel: {\n onCancel: function () {\n\n },\n text: \"Not now\"\n }\n });\n }\n }, function (response) {\n $interval.cancel(updateMigrationMessagesInterval);\n $scope.foo.isMigrating = false;\n $scope.foo.messages = [response.data.message];\n }\n );\n\n $scope.$on('$destroy', function () {\n if (angular.isDefined(updateMigrationMessagesInterval)) {\n $interval.cancel(updateMigrationMessagesInterval);\n }\n });\n\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n\n}\n","\r\nLoginController.$inject = [\"$scope\", \"RequestsErrorHandler\", \"$state\", \"HydraAuthService\", \"growl\"];angular\r\n .module('nzbhydraApp')\r\n .controller('LoginController', LoginController);\r\n\r\nfunction LoginController($scope, RequestsErrorHandler, $state, HydraAuthService, growl) {\r\n $scope.user = {};\r\n $scope.login = function () {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n HydraAuthService.login($scope.user.username, $scope.user.password).then(function () {\r\n HydraAuthService.setLoggedInByForm();\r\n growl.info(\"Login successful!\");\r\n $state.go(\"root.search\");\r\n }, function () {\r\n growl.error(\"Login failed!\")\r\n });\r\n });\r\n }\r\n}\r\n","\nIndexerStatusesController.$inject = [\"$scope\", \"$http\", \"statuses\"];\nformatDate.$inject = [\"dateFilter\"];angular\n .module('nzbhydraApp')\n .controller('IndexerStatusesController', IndexerStatusesController);\n\nfunction IndexerStatusesController($scope, $http, statuses) {\n $scope.statuses = statuses.data;\n $scope.expiryWarnings = {};\n\n $scope.formatState = function (state) {\n if (state === \"ENABLED\") {\n return \"Enabled\";\n } else if (state === \"DISABLED_SYSTEM_TEMPORARY\") {\n return \"Temporarily disabled by system\";\n } else if (state === \"DISABLED_SYSTEM\") {\n return \"Disabled by system\";\n } else {\n return \"Disabled by user\";\n }\n };\n\n $scope.getLabelClass = function (state) {\n if (state === \"ENABLED\") {\n return \"primary\";\n } else if (state === \"DISABLED_SYSTEM_TEMPORARY\") {\n return \"warning\";\n } else if (state === \"DISABLED_SYSTEM\") {\n return \"danger\";\n } else {\n return \"default\";\n }\n };\n\n $scope.isInPast = function (epochSeconds) {\n return epochSeconds < moment().unix();\n };\n\n\n _.each($scope.statuses, function (status) {\n if (status.vipExpirationDate != null && status.vipExpirationDate !== \"Lifetime\") {\n var expiryDate = moment(status.vipExpirationDate, \"YYYY-MM-DD\");\n var messagePrefix = \"VIP access\";\n if (expiryDate < moment()) {\n status.expiryWarning = messagePrefix + \" expired\";\n } else if (expiryDate.subtract(7, 'days') < moment()) {\n status.expiryWarning = messagePrefix + \" will expire in the next 7 days\";\n }\n console.log(status.expiryWarning);\n }\n }\n )\n ;\n}\n\nangular\n .module('nzbhydraApp')\n .filter('formatDate', formatDate);\n\nfunction formatDate(dateFilter) {\n return function (timestamp, hidePast) {\n if (timestamp) {\n if (timestamp * 1000 < (new Date).getTime() && hidePast) {\n return \"\"; //\n }\n\n var t = timestamp * 1000;\n t = dateFilter(t, 'yyyy-MM-dd HH:mm');\n return t;\n } else {\n return \"\";\n }\n }\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDate', reformatDate);\n\nfunction reformatDate() {\n return function (date, format) {\n if (!date) {\n return \"\";\n }\n if (angular.isUndefined(format)) {\n format = \"YYYY-MM-DD HH:mm\";\n }\n //Date in database is saved as UTC without timezone information\n return moment.unix(date).local().format(format);\n }\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateSeconds', reformatDateSeconds);\n\nfunction reformatDateSeconds() {\n return function (date, format) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm:ss\");\n }\n}\n\n\nangular\n .module('nzbhydraApp')\n .filter('humanizeDate', humanizeDate);\n\nfunction humanizeDate() {\n return function (date) {\n return moment().to(moment.unix(date));\n }\n}","\r\nIndexController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\"];angular\r\n .module('nzbhydraApp')\r\n .controller('IndexController', IndexController);\r\n\r\nfunction IndexController($scope, $http, $stateParams, $state) {\r\n\r\n $state.go(\"root.search\");\r\n}\r\n","\r\nHydraAuthService.$inject = [\"$q\", \"$rootScope\", \"$http\", \"bootstrapped\", \"$httpParamSerializerJQLike\", \"$state\"];angular\r\n .module('nzbhydraApp')\r\n .factory('HydraAuthService', HydraAuthService);\r\n\r\nfunction HydraAuthService($q, $rootScope, $http, bootstrapped, $httpParamSerializerJQLike, $state) {\r\n\r\n var loggedIn = bootstrapped.username;\r\n\r\n\r\n return {\r\n isLoggedIn: isLoggedIn,\r\n login: login,\r\n askForPassword: askForPassword,\r\n logout: logout,\r\n setLoggedInByForm: setLoggedInByForm,\r\n getUserRights: getUserRights,\r\n setLoggedInByBasic: setLoggedInByBasic,\r\n getUserName: getUserName,\r\n getUserInfos: getUserInfos\r\n };\r\n\r\n function getUserInfos() {\r\n return bootstrapped;\r\n }\r\n\r\n function isLoggedIn() {\r\n return bootstrapped.username;\r\n }\r\n\r\n function setLoggedInByForm() {\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n }\r\n\r\n\r\n function setLoggedInByBasic(_maySeeStats, _maySeeAdmin, _username) {\r\n }\r\n\r\n function login(username, password) {\r\n var deferred = $q.defer();\r\n //return $http.post(\"login\", data = {username: username, password: password})\r\n return $http({\r\n url: \"login\",\r\n method: \"POST\",\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded' // Note the appropriate header\r\n },\r\n data: $httpParamSerializerJQLike({username: username, password: password})\r\n })\r\n .then(function () {\r\n $http.get(\"internalapi/userinfos\").then(function (data) {\r\n bootstrapped = data.data;\r\n loggedIn = true;\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n deferred.resolve();\r\n });\r\n });\r\n }\r\n\r\n function askForPassword(params) {\r\n return $http.get(\"internalapi/askpassword\", {params: params}).then(function (data) {\r\n bootstrapped = data.data;\r\n return bootstrapped;\r\n });\r\n }\r\n\r\n function logout() {\r\n var deferred = $q.defer();\r\n return $http.post(\"logout\").then(function () {\r\n $http.get(\"internalapi/userinfos\").then(function (data) {\r\n bootstrapped = data.data;\r\n $rootScope.$broadcast(\"user:loggedOut\");\r\n loggedIn = false;\r\n if (bootstrapped.maySeeSearch) {\r\n $state.go(\"root.search\");\r\n } else {\r\n $state.go(\"root.login\");\r\n }\r\n //window.location.reload(false);\r\n deferred.resolve();\r\n });\r\n });\r\n }\r\n\r\n function getUserRights() {\r\n var userInfos = getUserInfos();\r\n return {\r\n maySeeStats: userInfos.maySeeStats,\r\n maySeeAdmin: userInfos.maySeeAdmin,\r\n maySeeSearch: userInfos.maySeeSearch\r\n };\r\n }\r\n\r\n function getUserName() {\r\n return bootstrapped.username;\r\n }\r\n\r\n\r\n}","\nHeaderController.$inject = [\"$scope\", \"$state\", \"growl\", \"HydraAuthService\", \"bootstrapped\"];angular\n .module('nzbhydraApp')\n .controller('HeaderController', HeaderController);\n\nfunction HeaderController($scope, $state, growl, HydraAuthService, bootstrapped) {\n\n\n $scope.showLoginout = false;\n $scope.oldUserName = null;\n $scope.bootstrapped = bootstrapped;\n\n function update(event) {\n\n $scope.userInfos = HydraAuthService.getUserInfos();\n if (!$scope.userInfos.authConfigured) {\n $scope.showSearch = true;\n $scope.showAdmin = true;\n $scope.showStats = true;\n $scope.showLoginout = false;\n } else {\n if ($scope.userInfos.username) {\n $scope.showSearch = true;\n $scope.showAdmin = $scope.userInfos.maySeeAdmin || !$scope.userInfos.adminRestricted;\n $scope.showStats = $scope.userInfos.maySeeStats || !$scope.userInfos.statsRestricted;\n $scope.showLoginout = true;\n $scope.username = $scope.userInfos.username;\n $scope.loginlogoutText = \"Logout \" + $scope.username;\n $scope.oldUserName = $scope.username;\n } else {\n $scope.showAdmin = !$scope.userInfos.adminRestricted;\n $scope.showStats = !$scope.userInfos.statsRestricted;\n $scope.showSearch = !$scope.userInfos.searchRestricted;\n $scope.loginlogoutText = \"Login\";\n $scope.showLoginout = ($scope.userInfos.adminRestricted || $scope.userInfos.statsRestricted || $scope.userInfos.searchRestricted) && event !== \"loggedOut\" && !$state.is(\"root.login\");\n $scope.username = \"\";\n }\n }\n }\n\n update();\n\n\n $scope.$on(\"user:loggedIn\", function (event, data) {\n update(\"loggedIn\");\n });\n\n $scope.$on(\"user:loggedOut\", function (event, data) {\n update(\"loggedOut\");\n });\n\n $scope.loginout = function () {\n if (HydraAuthService.isLoggedIn()) {\n HydraAuthService.logout().then(function () {\n if ($scope.userInfos.authType === \"BASIC\") {\n growl.info(\"Logged out. Close your browser to make sure session is closed.\");\n }\n else if ($scope.userInfos.authType === \"FORM\") {\n growl.info(\"Logged out\");\n }\n update();\n //$state.go(\"root.search\", null, {reload: true});\n });\n\n } else {\n if ($scope.userInfos.authType === \"BASIC\") {\n var params = {};\n if ($scope.oldUserName) {\n params = {\n old_username: $scope.oldUserName\n }\n }\n HydraAuthService.askForPassword(params).then(function () {\n growl.info(\"Login successful!\");\n $scope.oldUserName = null;\n update(\"loggedIn\");\n $state.go(\"root.search\");\n })\n } else if ($scope.userInfos.authType === \"FORM\") {\n $state.go(\"root.login\");\n } else {\n growl.info(\"You shouldn't need to login but here you go!\");\n }\n }\n\n };\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n//\nGenericStorageService.$inject = [\"$http\"];\nangular\n .module('nzbhydraApp')\n .factory('GenericStorageService', GenericStorageService);\n\nfunction GenericStorageService($http) {\n\n return {\n get: get,\n put: put\n };\n\n function get(key, forUser) {\n return $http.get(\"internalapi/genericstorage/\" + key, {params: {forUser: forUser}, ignoreLoadingBar: true});\n }\n\n function put(key, forUser, value) {\n return $http.put(\"internalapi/genericstorage/\" + key, value, {params: {forUser: forUser}, ignoreLoadingBar: true});\n }\n\n\n}","var HEADER_NAME = 'NzbHydra2-Handle-Errors-Generically';\nvar specificallyHandleInProgress = false;\n\nnzbhydraapp.factory('RequestsErrorHandler', [\"$q\", \"growl\", \"blockUI\", \"GeneralModalService\", function ($q, growl, blockUI, GeneralModalService) {\n return {\n // --- The user's API for claiming responsiblity for requests ---\n specificallyHandled: function (specificallyHandledBlock) {\n specificallyHandleInProgress = true;\n try {\n return specificallyHandledBlock();\n } finally {\n specificallyHandleInProgress = false;\n }\n },\n\n // --- Response interceptor for handling errors generically ---\n responseError: function (rejection) {\n blockUI.reset();\n var shouldHandle = (rejection && rejection.config && rejection.status !== 403 && rejection.config.headers && rejection.config.headers[HEADER_NAME] && !rejection.config.url.contains(\"logerror\") && !rejection.config.url.contains(\"/ping\") && !rejection.config.alreadyHandled);\n if (shouldHandle) {\n if (rejection.data) {\n\n var message = \"An error occurred:
          \" + rejection.data.status + \": \" + rejection.data.error;\n if (rejection.data.path) {\n message += \"

          Path: \" + rejection.data.path;\n }\n if (message !== \"No message available\") {\n message += \"

          Message: \" + rejection.data.message;\n } else {\n message += \"

          Exception: \" + rejection.data.exception;\n }\n } else {\n message = \"An unknown error occurred while communicating with NZBHydra:

          \" + JSON.stringify(rejection);\n }\n GeneralModalService.open(message);\n\n } else if (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && rejection.config.url.contains(\"logerror\")) {\n console.log(\"Not handling connection error while sending exception to server\");\n }\n\n return $q.reject(rejection);\n }\n };\n}]);\n\nnzbhydraapp.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {\n $httpProvider.interceptors.push('RequestsErrorHandler');\n\n // --- Decorate $http to add a special header by default ---\n\n function addHeaderToConfig(config) {\n config = config || {};\n config.headers = config.headers || {};\n\n // Add the header unless user asked to handle errors himself\n if (!specificallyHandleInProgress) {\n config.headers[HEADER_NAME] = true;\n }\n\n return config;\n }\n\n // The rest here is mostly boilerplate needed to decorate $http safely\n $provide.decorator('$http', ['$delegate', function ($delegate) {\n function decorateRegularCall(method) {\n return function (url, config) {\n return $delegate[method](url, addHeaderToConfig(config));\n };\n }\n\n function decorateDataCall(method) {\n return function (url, data, config) {\n return $delegate[method](url, data, addHeaderToConfig(config));\n };\n }\n\n function copyNotOverriddenAttributes(newHttp) {\n for (var attr in $delegate) {\n if (!newHttp.hasOwnProperty(attr)) {\n if (typeof($delegate[attr]) === 'function') {\n newHttp[attr] = function () {\n return $delegate.apply($delegate, arguments);\n };\n } else {\n newHttp[attr] = $delegate[attr];\n }\n }\n }\n }\n\n var newHttp = function (config) {\n return $delegate(addHeaderToConfig(config));\n };\n\n newHttp.get = decorateRegularCall('get');\n newHttp.delete = decorateRegularCall('delete');\n newHttp.head = decorateRegularCall('head');\n newHttp.jsonp = decorateRegularCall('jsonp');\n newHttp.post = decorateDataCall('post');\n newHttp.put = decorateDataCall('put');\n\n copyNotOverriddenAttributes(newHttp);\n\n return newHttp;\n }]);\n}]);","var filters = angular.module('filters', []);\n\nfilters.filter('bytes', function () {\n return function (bytes) {\n return filesize(bytes);\n }\n});\n\nfilters\n .filter('unsafe', ['$sce', function ($sce) {\n return function (text) {\n return $sce.trustAsHtml(text);\n };\n }]);\n\n","\r\nFileSelectionService.$inject = [\"$http\", \"$q\", \"$uibModal\"];angular\r\n .module('nzbhydraApp')\r\n .factory('FileSelectionService', FileSelectionService);\r\n\r\nfunction FileSelectionService($http, $q, $uibModal) {\r\n\r\n var categories = {};\r\n var selectedCategory = {};\r\n\r\n var service = {\r\n open: open\r\n };\r\n\r\n var deferred;\r\n\r\n return service;\r\n\r\n\r\n function open(fullPath, type) {\r\n var instance = $uibModal.open({\r\n templateUrl: 'static/html/file-selection.html',\r\n controller: 'FileSelectionModalController',\r\n size: \"md\",\r\n resolve: {\r\n data: function () {\r\n return $http.post(\"internalapi/config/folderlisting\", {\r\n fullPath: angular.isDefined(fullPath) ? fullPath : null,\r\n goUp: false,\r\n type: type\r\n });\r\n },\r\n type: function () {\r\n return type;\r\n }\r\n }\r\n });\r\n\r\n instance.result.then(function (selection) {\r\n deferred.resolve(selection);\r\n }, function () {\r\n deferred.reject(\"dismissed\");\r\n }\r\n );\r\n deferred = $q.defer();\r\n return deferred.promise;\r\n }\r\n\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').controller('FileSelectionModalController', [\"$scope\", \"$http\", \"$uibModalInstance\", \"FileSelectionService\", \"data\", \"type\", function ($scope, $http, $uibModalInstance, FileSelectionService, data, type) {\r\n\r\n $scope.type = type;\r\n $scope.showType = type === \"file\" ? \"File\" : \"Folder\";\r\n $scope.data = data.data;\r\n\r\n $scope.select = function (fileOrFolder, selectType) {\r\n if (selectType === \"file\" && type === \"file\") {\r\n $uibModalInstance.close(fileOrFolder.fullPath);\r\n } else if (selectType === \"folder\") {\r\n $http.post(\"internalapi/config/folderlisting\", {\r\n fullPath: fileOrFolder.fullPath,\r\n type: type,\r\n goUp: false\r\n }).then(function (data) {\r\n $scope.data = data.data;\r\n })\r\n }\r\n };\r\n\r\n $scope.goUp = function () {\r\n $http.post(\"internalapi/config/folderlisting\", {\r\n fullPath: $scope.data.fullPath,\r\n type: type,\r\n goUp: true\r\n }).then(function (data) {\r\n $scope.data = data.data;\r\n })\r\n };\r\n\r\n $scope.submit = function () {\r\n $uibModalInstance.close($scope.data.fullPath);\r\n }\r\n\r\n}]);","\r\nFileDownloadService.$inject = [\"$http\", \"growl\"];angular\r\n .module('nzbhydraApp')\r\n .factory('FileDownloadService', FileDownloadService);\r\n\r\nfunction FileDownloadService($http, growl) {\r\n\r\n var service = {\r\n downloadFile: downloadFile\r\n };\r\n\r\n return service;\r\n\r\n function downloadFile(link, filename, method, data) {\r\n return $http({\r\n method: method,\r\n url: link,\r\n data: data,\r\n responseType: 'arraybuffer'\r\n }).then(function (response, status, headers, config) {\r\n var a = document.createElement('a');\r\n var blob = new Blob([response.data], {'type': \"application/octet-stream\"});\r\n a.href = URL.createObjectURL(blob);\r\n a.download = filename;\r\n\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n }, function (data, status, headers, config) {\r\n growl.error(status);\r\n });\r\n\r\n }\r\n\r\n\r\n}\r\n\r\n","\r\nDownloaderCategoriesService.$inject = [\"$http\", \"$q\", \"$uibModal\"];angular\r\n .module('nzbhydraApp')\r\n .factory('DownloaderCategoriesService', DownloaderCategoriesService);\r\n\r\nfunction DownloaderCategoriesService($http, $q, $uibModal) {\r\n\r\n var categories = {};\r\n var selectedCategory = {};\r\n\r\n var service = {\r\n get: getCategories,\r\n invalidate: invalidate,\r\n select: select,\r\n openCategorySelection: openCategorySelection\r\n };\r\n\r\n var deferred;\r\n\r\n return service;\r\n\r\n function getCategories(downloader) {\r\n function loadAll() {\r\n if (downloader.name in categories) {\r\n var deferred = $q.defer();\r\n deferred.resolve(categories[downloader.name]);\r\n return deferred.promise;\r\n }\r\n\r\n return $http.get(encodeURI('internalapi/downloader/' + downloader.name + \"/categories\"))\r\n .then(function (categoriesResponse) {\r\n categories[downloader.name] = categoriesResponse.data;\r\n return categoriesResponse.data;\r\n\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n return loadAll().then(function (categories) {\r\n return categories;\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n\r\n function openCategorySelection(downloader) {\r\n var instance = $uibModal.open({\r\n templateUrl: 'static/html/directives/addable-nzb-modal.html',\r\n controller: 'DownloaderCategorySelectionController',\r\n size: \"sm\",\r\n resolve: {\r\n categories: function () {\r\n return getCategories(downloader)\r\n }\r\n }\r\n });\r\n\r\n instance.result.then(function () {\r\n }, function () {\r\n deferred.reject(\"dismissed\");\r\n }\r\n );\r\n deferred = $q.defer();\r\n return deferred.promise;\r\n }\r\n\r\n function select(category) {\r\n selectedCategory = category;\r\n\r\n deferred.resolve(category);\r\n }\r\n\r\n function invalidate() {\r\n categories = {};\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').controller('DownloaderCategorySelectionController', [\"$scope\", \"$uibModalInstance\", \"DownloaderCategoriesService\", \"categories\", function ($scope, $uibModalInstance, DownloaderCategoriesService, categories) {\r\n\r\n $scope.categories = categories;\r\n categories.sort();\r\n console.log(categories);\r\n $scope.select = function (category) {\r\n DownloaderCategoriesService.select(category);\r\n $uibModalInstance.close($scope);\r\n }\r\n}]);","\nDownloadHistoryController.$inject = [\"$scope\", \"StatsService\", \"downloads\", \"ConfigService\", \"$timeout\", \"$sce\"];angular\n .module('nzbhydraApp')\n .controller('DownloadHistoryController', DownloadHistoryController);\n\n\nfunction DownloadHistoryController($scope, StatsService, downloads, ConfigService, $timeout, $sce) {\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n var sortModel = {\n column: \"time\",\n sortMode: 2\n };\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n }, 10);\n $scope.filterModel = {};\n\n //Filter options\n $scope.indexersForFiltering = [];\n _.forEach(ConfigService.getSafe().indexers, function (indexer) {\n $scope.indexersForFiltering.push({label: indexer.name, id: indexer.name})\n });\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\n $scope.statusesForFiltering = [\n {label: \"None\", id: 'NONE'},\n {label: \"Requested\", id: 'REQUESTED'},\n {label: \"Internal error\", id: 'INTERNAL_ERROR'},\n {label: \"NZB downloaded successful\", id: 'NZB_DOWNLOAD_SUCCESSFUL'},\n {label: \"NZB download error\", id: 'NZB_DOWNLOAD_ERROR'},\n {label: \"NZB added\", id: 'NZB_ADDED'},\n {label: \"NZB not added\", id: 'NZB_NOT_ADDED'},\n {label: \"NZB add error\", id: 'NZB_ADD_ERROR'},\n {label: \"NZB add rejected\", id: 'NZB_ADD_REJECTED'},\n {label: \"Content download successful\", id: 'CONTENT_DOWNLOAD_SUCCESSFUL'},\n {label: \"Content download warning\", id: 'CONTENT_DOWNLOAD_WARNING'},\n {label: \"Content download error\", id: 'CONTENT_DOWNLOAD_ERROR'}\n ];\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: 'API'}, {\n label: \"Internal\",\n value: 'INTERNAL'\n }];\n\n //Preloaded data\n $scope.nzbDownloads = downloads.nzbDownloads;\n $scope.totalDownloads = downloads.totalDownloads;\n\n $scope.columnSizes = {\n time: 10,\n indexer: 10,\n title: 37,\n result: 9,\n source: 8,\n age: 6,\n username: 10,\n ip: 10\n };\n var anyUsername = false;\n var anyIp = false;\n for (var download in $scope.nzbDownloads) {\n if (download.username) {\n anyUsername = true;\n }\n if (download.ip) {\n anyIp = true;\n }\n if (anyIp && anyUsername) {\n break;\n }\n }\n\n if (ConfigService.getSafe().logging.historyUserInfoType === \"NONE\" || (!anyUsername && !anyIp)) {\n $scope.columnSizes.username = 0;\n $scope.columnSizes.ip = 0;\n $scope.columnSizes.title += 20;\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"IP\") {\n $scope.columnSizes.username = 0;\n $scope.columnSizes.title += 10;\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"USERNAME\") {\n $scope.columnSizes.ip = 0;\n $scope.columnSizes.title += 10;\n }\n\n\n $scope.update = function () {\n StatsService.getDownloadHistory($scope.pagination.current, $scope.limit, $scope.filterModel, sortModel).then(function (downloads) {\n $scope.nzbDownloads = downloads.nzbDownloads;\n $scope.totalDownloads = downloads.totalDownloads;\n });\n };\n\n\n $scope.$on(\"sort\", function (event, column, sortMode) {\n if (sortMode === 0) {\n column = \"time\";\n sortMode = 2;\n }\n sortModel = {\n column: column,\n sortMode: sortMode\n };\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n $scope.update();\n });\n\n $scope.getStatusIcon = function (result) {\n var spans;\n if (result === \"NONE\" || result === \"REQUESTED\") {\n spans = ''\n }\n if (result === \"INTERNAL_ERROR\") {\n spans = ''\n }\n if (result === \"INTERNAL_ERROR\") {\n spans = ''\n }\n if (result === 'NZB_DOWNLOAD_SUCCESSFUL') {\n spans = '';\n }\n if (result === 'NZB_DOWNLOAD_ERROR') {\n spans = '';\n }\n if (result === 'NZB_ADDED') {\n spans = '';\n }\n if (result === 'NZB_NOT_ADDED' || result === 'NZB_ADD_ERROR' || result === 'NZB_ADD_REJECTED') {\n spans = '';\n }\n if (result === 'CONTENT_DOWNLOAD_SUCCESSFUL') {\n spans = '';\n }\n if (result === 'CONTENT_DOWNLOAD_ERROR' || result === 'CONTENT_DOWNLOAD_WARNING') {\n spans = '';\n }\n return $sce.trustAsHtml('' + spans + '');\n\n };\n\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filterValue) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n $scope.update();\n })\n\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateEpoch', reformatDateEpoch);\n\nfunction reformatDateEpoch() {\n return function (date) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm\");\n\n }\n}\n","/*\r\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n\r\nDebugService.$inject = [\"$filter\"];\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('DebugService', DebugService);\r\n\r\nfunction DebugService($filter) {\r\n\r\n var debug = {};\r\n\r\n return {\r\n log: log,\r\n print: print\r\n };\r\n\r\n function log(name) {\r\n if (!(name in debug)) {\r\n debug[name] = {first: new Date().getTime(), last: new Date().getTime()};\r\n } else {\r\n debug[name][\"last\"] = new Date().getTime();\r\n }\r\n }\r\n\r\n function print() {\r\n //Re-enable if necessary\r\n // for (var key in debug) {\r\n // if (debug.hasOwnProperty(key)) {\r\n // console.log(\"First \" + key + \": \" + $filter(\"date\")(new Date(debug[key][\"first\"]), \"h:mm:ss:sss\"));\r\n // console.log(\"Last \" + key + \": \" + $filter(\"date\")(new Date(debug[key][\"last\"]), \"h:mm:ss:sss\"));\r\n // console.log(\"Diff: \" + (debug[key][\"last\"] - debug[key][\"first\"]));\r\n // }\r\n // }\r\n }\r\n\r\n\r\n}","\r\nCategoriesService.$inject = [\"ConfigService\"];angular\r\n .module('nzbhydraApp')\r\n .factory('CategoriesService', CategoriesService);\r\n\r\nfunction CategoriesService(ConfigService) {\r\n\r\n return {\r\n getByName: getByName,\r\n getAllCategories: getAllCategories,\r\n getDefault: getDefault,\r\n getWithoutAll: getWithoutAll\r\n };\r\n\r\n\r\n function getByName(name) {\r\n for (var cat in ConfigService.getSafe().categoriesConfig.categories) {\r\n var category = ConfigService.getSafe().categoriesConfig.categories[cat];\r\n if (category.name === name) {\r\n return category;\r\n }\r\n }\r\n }\r\n\r\n function getAllCategories() {\r\n return ConfigService.getSafe().categoriesConfig.categories;\r\n }\r\n\r\n function getWithoutAll() {\r\n var cats = ConfigService.getSafe().categoriesConfig.categories;\r\n return cats.slice(1, cats.length);\r\n }\r\n\r\n function getDefault() {\r\n return getByName(ConfigService.getSafe().categoriesConfig.defaultCategory);\r\n }\r\n\r\n}","\r\nBackupService.$inject = [\"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('BackupService', BackupService);\r\n\r\nfunction BackupService($http) {\r\n\r\n return {\r\n getBackupsList: getBackupsList,\r\n restoreFromFile: restoreFromFile\r\n };\r\n\r\n\r\n function getBackupsList() {\r\n return $http.get('internalapi/backup/list').then(function (response) {\r\n return response.data;\r\n });\r\n }\r\n\r\n function restoreFromFile(filename) {\r\n return $http.get('internalapi/backup/restore', {params: {filename: filename}}).then(function (response) {\r\n return response;\r\n });\r\n }\r\n\r\n}","//Copied from https://github.com/oblador/angular-scroll because installing it via bower caused errors\nvar duScrollDefaultEasing = function (x) {\n\n\n if (x < 0.5) {\n return Math.pow(x * 2, 2) / 2;\n }\n return 1 - Math.pow((1 - x) * 2, 2) / 2;\n};\n\nvar duScroll = angular.module('duScroll', [\n 'duScroll.scrollspy',\n 'duScroll.smoothScroll',\n 'duScroll.scrollContainer',\n 'duScroll.spyContext',\n 'duScroll.scrollHelpers'\n])\n//Default animation duration for smoothScroll directive\n .value('duScrollDuration', 350)\n //Scrollspy debounce interval, set to 0 to disable\n .value('duScrollSpyWait', 100)\n //Scrollspy forced refresh interval, use if your content changes or reflows without scrolling.\n //0 to disable\n .value('duScrollSpyRefreshInterval', 0)\n //Wether or not multiple scrollspies can be active at once\n .value('duScrollGreedy', false)\n //Default offset for smoothScroll directive\n .value('duScrollOffset', 0)\n //Default easing function for scroll animation\n .value('duScrollEasing', duScrollDefaultEasing)\n //Which events on the container (such as body) should cancel scroll animations\n .value('duScrollCancelOnEvents', 'scroll mousedown mousewheel touchmove keydown')\n //Whether or not to activate the last scrollspy, when page/container bottom is reached\n .value('duScrollBottomSpy', false)\n //Active class name\n .value('duScrollActiveClass', 'active');\n\nif (typeof module !== 'undefined' && module && module.exports) {\n module.exports = duScroll;\n}\n\n\nangular.module('duScroll.scrollHelpers', ['duScroll.requestAnimation'])\n .run([\"$window\", \"$q\", \"cancelAnimation\", \"requestAnimation\", \"duScrollEasing\", \"duScrollDuration\", \"duScrollOffset\", \"duScrollCancelOnEvents\", function ($window, $q, cancelAnimation, requestAnimation, duScrollEasing, duScrollDuration, duScrollOffset, duScrollCancelOnEvents) {\n 'use strict';\n\n var proto = {};\n\n var isDocument = function (el) {\n return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE);\n };\n\n var isElement = function (el) {\n return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE);\n };\n\n var unwrap = function (el) {\n return isElement(el) || isDocument(el) ? el : el[0];\n };\n\n proto.duScrollTo = function (left, top, duration, easing) {\n var aliasFn;\n if (angular.isElement(left)) {\n aliasFn = this.duScrollToElement;\n } else if (angular.isDefined(duration)) {\n aliasFn = this.duScrollToAnimated;\n }\n if (aliasFn) {\n return aliasFn.apply(this, arguments);\n }\n var el = unwrap(this);\n if (isDocument(el)) {\n return $window.scrollTo(left, top);\n }\n el.scrollLeft = left;\n el.scrollTop = top;\n };\n\n var scrollAnimation, deferred;\n proto.duScrollToAnimated = function (left, top, duration, easing) {\n if (duration && !easing) {\n easing = duScrollEasing;\n }\n var startLeft = this.duScrollLeft(),\n startTop = this.duScrollTop(),\n deltaLeft = Math.round(left - startLeft),\n deltaTop = Math.round(top - startTop);\n\n var startTime = null, progress = 0;\n var el = this;\n\n var cancelScrollAnimation = function ($event) {\n if (!$event || (progress && $event.which > 0)) {\n if (duScrollCancelOnEvents) {\n el.unbind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n cancelAnimation(scrollAnimation);\n deferred.reject();\n scrollAnimation = null;\n }\n };\n\n if (scrollAnimation) {\n cancelScrollAnimation();\n }\n deferred = $q.defer();\n\n if (duration === 0 || (!deltaLeft && !deltaTop)) {\n if (duration === 0) {\n el.duScrollTo(left, top);\n }\n deferred.resolve();\n return deferred.promise;\n }\n\n var animationStep = function (timestamp) {\n if (startTime === null) {\n startTime = timestamp;\n }\n\n progress = timestamp - startTime;\n var percent = (progress >= duration ? 1 : easing(progress / duration));\n\n el.scrollTo(\n startLeft + Math.ceil(deltaLeft * percent),\n startTop + Math.ceil(deltaTop * percent)\n );\n if (percent < 1) {\n scrollAnimation = requestAnimation(animationStep);\n } else {\n if (duScrollCancelOnEvents) {\n el.unbind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n scrollAnimation = null;\n deferred.resolve();\n }\n };\n\n //Fix random mobile safari bug when scrolling to top by hitting status bar\n el.duScrollTo(startLeft, startTop);\n\n if (duScrollCancelOnEvents) {\n el.bind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n\n scrollAnimation = requestAnimation(animationStep);\n return deferred.promise;\n };\n\n proto.duScrollToElement = function (target, offset, duration, easing) {\n var el = unwrap(this);\n if (!angular.isNumber(offset) || isNaN(offset)) {\n offset = duScrollOffset;\n }\n var top = this.duScrollTop() + unwrap(target).getBoundingClientRect().top - offset;\n if (isElement(el)) {\n top -= el.getBoundingClientRect().top;\n }\n return this.duScrollTo(0, top, duration, easing);\n };\n\n proto.duScrollLeft = function (value, duration, easing) {\n if (angular.isNumber(value)) {\n return this.duScrollTo(value, this.duScrollTop(), duration, easing);\n }\n var el = unwrap(this);\n if (isDocument(el)) {\n return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft;\n }\n return el.scrollLeft;\n };\n proto.duScrollTop = function (value, duration, easing) {\n if (angular.isNumber(value)) {\n return this.duScrollTo(this.duScrollLeft(), value, duration, easing);\n }\n var el = unwrap(this);\n if (isDocument(el)) {\n return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;\n }\n return el.scrollTop;\n };\n\n proto.duScrollToElementAnimated = function (target, offset, duration, easing) {\n return this.duScrollToElement(target, offset, duration || duScrollDuration, easing);\n };\n\n proto.duScrollTopAnimated = function (top, duration, easing) {\n return this.duScrollTop(top, duration || duScrollDuration, easing);\n };\n\n proto.duScrollLeftAnimated = function (left, duration, easing) {\n return this.duScrollLeft(left, duration || duScrollDuration, easing);\n };\n\n angular.forEach(proto, function (fn, key) {\n angular.element.prototype[key] = fn;\n\n //Remove prefix if not already claimed by jQuery / ui.utils\n var unprefixed = key.replace(/^duScroll/, 'scroll');\n if (angular.isUndefined(angular.element.prototype[unprefixed])) {\n angular.element.prototype[unprefixed] = fn;\n }\n });\n\n }]);\n\n\n//Adapted from https://gist.github.com/paulirish/1579671\nangular.module('duScroll.polyfill', [])\n .factory('polyfill', [\"$window\", function ($window) {\n 'use strict';\n\n var vendors = ['webkit', 'moz', 'o', 'ms'];\n\n return function (fnName, fallback) {\n if ($window[fnName]) {\n return $window[fnName];\n }\n var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1);\n for (var key, i = 0; i < vendors.length; i++) {\n key = vendors[i] + suffix;\n if ($window[key]) {\n return $window[key];\n }\n }\n return fallback;\n };\n }]);\n\nangular.module('duScroll.requestAnimation', ['duScroll.polyfill'])\n .factory('requestAnimation', [\"polyfill\", \"$timeout\", function (polyfill, $timeout) {\n 'use strict';\n\n var lastTime = 0;\n var fallback = function (callback, element) {\n var currTime = new Date().getTime();\n var timeToCall = Math.max(0, 16 - (currTime - lastTime));\n var id = $timeout(function () {\n callback(currTime + timeToCall);\n },\n timeToCall);\n lastTime = currTime + timeToCall;\n return id;\n };\n\n return polyfill('requestAnimationFrame', fallback);\n }])\n .factory('cancelAnimation', [\"polyfill\", \"$timeout\", function (polyfill, $timeout) {\n 'use strict';\n\n var fallback = function (promise) {\n $timeout.cancel(promise);\n };\n\n return polyfill('cancelAnimationFrame', fallback);\n }]);\n\n\nangular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI'])\n .factory('spyAPI', [\"$rootScope\", \"$timeout\", \"$interval\", \"$window\", \"$document\", \"scrollContainerAPI\", \"duScrollGreedy\", \"duScrollSpyWait\", \"duScrollSpyRefreshInterval\", \"duScrollBottomSpy\", \"duScrollActiveClass\", function ($rootScope, $timeout, $interval, $window, $document, scrollContainerAPI, duScrollGreedy, duScrollSpyWait, duScrollSpyRefreshInterval, duScrollBottomSpy, duScrollActiveClass) {\n 'use strict';\n\n var createScrollHandler = function (context) {\n var timer = false, queued = false;\n var handler = function () {\n queued = false;\n var container = context.container,\n containerEl = container[0],\n containerOffset = 0,\n bottomReached;\n\n if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) {\n containerOffset = containerEl.getBoundingClientRect().top;\n bottomReached = Math.round(containerEl.scrollTop + containerEl.clientHeight) >= containerEl.scrollHeight;\n } else {\n var documentScrollHeight = $document[0].body.scrollHeight || $document[0].documentElement.scrollHeight; // documentElement for IE11\n bottomReached = Math.round($window.pageYOffset + $window.innerHeight) >= documentScrollHeight;\n }\n var compareProperty = (duScrollBottomSpy && bottomReached ? 'bottom' : 'top');\n\n var i, currentlyActive, toBeActive, spies, spy, pos;\n spies = context.spies;\n currentlyActive = context.currentlyActive;\n toBeActive = undefined;\n\n for (i = 0; i < spies.length; i++) {\n spy = spies[i];\n pos = spy.getTargetPosition();\n if (!pos || !spy.$element) continue;\n\n if ((duScrollBottomSpy && bottomReached) || (pos.top + spy.offset - containerOffset < 20 && (duScrollGreedy || pos.top * -1 + containerOffset) < pos.height)) {\n //Find the one closest the viewport top or the page bottom if it's reached\n if (!toBeActive || toBeActive[compareProperty] < pos[compareProperty]) {\n toBeActive = {\n spy: spy\n };\n toBeActive[compareProperty] = pos[compareProperty];\n }\n }\n }\n\n if (toBeActive) {\n toBeActive = toBeActive.spy;\n }\n if (currentlyActive === toBeActive || (duScrollGreedy && !toBeActive)) return;\n if (currentlyActive && currentlyActive.$element) {\n currentlyActive.$element.removeClass(duScrollActiveClass);\n $rootScope.$broadcast(\n 'duScrollspy:becameInactive',\n currentlyActive.$element,\n angular.element(currentlyActive.getTargetElement())\n );\n }\n if (toBeActive) {\n toBeActive.$element.addClass(duScrollActiveClass);\n $rootScope.$broadcast(\n 'duScrollspy:becameActive',\n toBeActive.$element,\n angular.element(toBeActive.getTargetElement())\n );\n }\n context.currentlyActive = toBeActive;\n };\n\n if (!duScrollSpyWait) {\n return handler;\n }\n\n //Debounce for potential performance savings\n return function () {\n if (!timer) {\n handler();\n timer = $timeout(function () {\n timer = false;\n if (queued) {\n handler();\n }\n }, duScrollSpyWait, false);\n } else {\n queued = true;\n }\n };\n };\n\n var contexts = {};\n\n var createContext = function ($scope) {\n var id = $scope.$id;\n var context = {\n spies: []\n };\n\n context.handler = createScrollHandler(context);\n contexts[id] = context;\n\n $scope.$on('$destroy', function () {\n destroyContext($scope);\n });\n\n return id;\n };\n\n var destroyContext = function ($scope) {\n var id = $scope.$id;\n var context = contexts[id], container = context.container;\n if (context.intervalPromise) {\n $interval.cancel(context.intervalPromise);\n }\n if (container) {\n container.off('scroll', context.handler);\n }\n delete contexts[id];\n };\n\n var defaultContextId = createContext($rootScope);\n\n var getContextForScope = function (scope) {\n if (contexts[scope.$id]) {\n return contexts[scope.$id];\n }\n if (scope.$parent) {\n return getContextForScope(scope.$parent);\n }\n return contexts[defaultContextId];\n };\n\n var getContextForSpy = function (spy) {\n var context, contextId, scope = spy.$scope;\n if (scope) {\n return getContextForScope(scope);\n }\n //No scope, most likely destroyed\n for (contextId in contexts) {\n context = contexts[contextId];\n if (context.spies.indexOf(spy) !== -1) {\n return context;\n }\n }\n };\n\n var isElementInDocument = function (element) {\n while (element.parentNode) {\n element = element.parentNode;\n if (element === document) {\n return true;\n }\n }\n return false;\n };\n\n var addSpy = function (spy) {\n var context = getContextForSpy(spy);\n if (!context) return;\n context.spies.push(spy);\n if (!context.container || !isElementInDocument(context.container)) {\n if (context.container) {\n context.container.off('scroll', context.handler);\n }\n context.container = scrollContainerAPI.getContainer(spy.$scope);\n if (duScrollSpyRefreshInterval && !context.intervalPromise) {\n context.intervalPromise = $interval(context.handler, duScrollSpyRefreshInterval, 0, false);\n }\n context.container.on('scroll', context.handler).triggerHandler('scroll');\n }\n };\n\n var removeSpy = function (spy) {\n var context = getContextForSpy(spy);\n if (spy === context.currentlyActive) {\n $rootScope.$broadcast('duScrollspy:becameInactive', context.currentlyActive.$element);\n context.currentlyActive = null;\n }\n var i = context.spies.indexOf(spy);\n if (i !== -1) {\n context.spies.splice(i, 1);\n }\n spy.$element = null;\n };\n\n return {\n addSpy: addSpy,\n removeSpy: removeSpy,\n createContext: createContext,\n destroyContext: destroyContext,\n getContextForScope: getContextForScope\n };\n }]);\n\n\nangular.module('duScroll.scrollContainerAPI', [])\n .factory('scrollContainerAPI', [\"$document\", function ($document) {\n 'use strict';\n\n var containers = {};\n\n var setContainer = function (scope, element) {\n var id = scope.$id;\n containers[id] = element;\n return id;\n };\n\n var getContainerId = function (scope) {\n if (containers[scope.$id]) {\n return scope.$id;\n }\n if (scope.$parent) {\n return getContainerId(scope.$parent);\n }\n\n };\n\n var getContainer = function (scope) {\n var id = getContainerId(scope);\n return id ? containers[id] : $document;\n };\n\n var removeContainer = function (scope) {\n var id = getContainerId(scope);\n if (id) {\n delete containers[id];\n }\n };\n\n return {\n getContainerId: getContainerId,\n getContainer: getContainer,\n setContainer: setContainer,\n removeContainer: removeContainer\n };\n }]);\n\n\nangular.module('duScroll.smoothScroll', ['duScroll.scrollHelpers', 'duScroll.scrollContainerAPI'])\n .directive('duSmoothScroll', [\"duScrollDuration\", \"duScrollOffset\", \"scrollContainerAPI\", function (duScrollDuration, duScrollOffset, scrollContainerAPI) {\n 'use strict';\n\n return {\n link: function ($scope, $element, $attr) {\n $element.on('click', function (e) {\n if ((!$attr.href || $attr.href.indexOf('#') === -1) && $attr.duSmoothScroll === '') return;\n\n var id = $attr.href ? $attr.href.replace(/.*(?=#[^\\s]+$)/, '').substring(1) : $attr.duSmoothScroll;\n\n var target = document.getElementById(id) || document.getElementsByName(id)[0];\n if (!target || !target.getBoundingClientRect) return;\n\n if (e.stopPropagation) e.stopPropagation();\n if (e.preventDefault) e.preventDefault();\n\n var offset = $attr.offset ? parseInt($attr.offset, 10) : duScrollOffset;\n var duration = $attr.duration ? parseInt($attr.duration, 10) : duScrollDuration;\n var container = scrollContainerAPI.getContainer($scope);\n\n container.duScrollToElement(\n angular.element(target),\n isNaN(offset) ? 0 : offset,\n isNaN(duration) ? 0 : duration\n );\n });\n }\n };\n }]);\n\n\nangular.module('duScroll.spyContext', ['duScroll.spyAPI'])\n .directive('duSpyContext', [\"spyAPI\", function (spyAPI) {\n 'use strict';\n\n return {\n restrict: 'A',\n scope: true,\n compile: function compile(tElement, tAttrs, transclude) {\n return {\n pre: function preLink($scope, iElement, iAttrs, controller) {\n spyAPI.createContext($scope);\n }\n };\n }\n };\n }]);\n\n\nangular.module('duScroll.scrollContainer', ['duScroll.scrollContainerAPI'])\n .directive('duScrollContainer', [\"scrollContainerAPI\", function (scrollContainerAPI) {\n 'use strict';\n\n return {\n restrict: 'A',\n scope: true,\n compile: function compile(tElement, tAttrs, transclude) {\n return {\n pre: function preLink($scope, iElement, iAttrs, controller) {\n iAttrs.$observe('duScrollContainer', function (element) {\n if (angular.isString(element)) {\n element = document.getElementById(element);\n }\n\n element = (angular.isElement(element) ? angular.element(element) : iElement);\n scrollContainerAPI.setContainer($scope, element);\n $scope.$on('$destroy', function () {\n scrollContainerAPI.removeContainer($scope);\n });\n });\n }\n };\n }\n };\n }]);\n\n\nangular.module('duScroll.scrollspy', ['duScroll.spyAPI'])\n .directive('duScrollspy', [\"spyAPI\", \"duScrollOffset\", \"$timeout\", \"$rootScope\", function (spyAPI, duScrollOffset, $timeout, $rootScope) {\n 'use strict';\n\n var Spy = function (targetElementOrId, $scope, $element, offset) {\n if (angular.isElement(targetElementOrId)) {\n this.target = targetElementOrId;\n } else if (angular.isString(targetElementOrId)) {\n this.targetId = targetElementOrId;\n }\n this.$scope = $scope;\n this.$element = $element;\n this.offset = offset;\n };\n\n Spy.prototype.getTargetElement = function () {\n if (!this.target && this.targetId) {\n this.target = document.getElementById(this.targetId) || document.getElementsByName(this.targetId)[0];\n }\n return this.target;\n };\n\n Spy.prototype.getTargetPosition = function () {\n var target = this.getTargetElement();\n if (target) {\n return target.getBoundingClientRect();\n }\n };\n\n Spy.prototype.flushTargetCache = function () {\n if (this.targetId) {\n this.target = undefined;\n }\n };\n\n return {\n link: function ($scope, $element, $attr) {\n var href = $attr.ngHref || $attr.href;\n var targetId;\n\n if (href && href.indexOf('#') !== -1) {\n targetId = href.replace(/.*(?=#[^\\s]+$)/, '').substring(1);\n } else if ($attr.duScrollspy) {\n targetId = $attr.duScrollspy;\n } else if ($attr.duSmoothScroll) {\n targetId = $attr.duSmoothScroll;\n }\n if (!targetId) return;\n\n // Run this in the next execution loop so that the scroll context has a chance\n // to initialize\n var timeoutPromise = $timeout(function () {\n var spy = new Spy(targetId, $scope, $element, -($attr.offset ? parseInt($attr.offset, 10) : duScrollOffset));\n spyAPI.addSpy(spy);\n\n $scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy));\n var deregisterOnStateChange = $rootScope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy));\n $scope.$on('$destroy', function () {\n spyAPI.removeSpy(spy);\n deregisterOnStateChange();\n });\n }, 0, false);\n $scope.$on('$destroy', function () {\n $timeout.cancel(timeoutPromise);\n });\n }\n };\n }]);\n"]} \ No newline at end of file +{"version":3,"sources":["nzbhydra.js","directives/tasks.js","directives/tab-or-chart.js","directives/selection-button.js","directives/search-result.js","directives/save-or-send-torrent.js","directives/on-finish-render.js","directives/multiselect-dropdown.js","directives/keep-focus.js","directives/indexer-state-switch.js","directives/indexer-selection-button.js","directives/indexer-input.js","directives/hydra-updates.js","directives/hydra-news.js","directives/hydra-log.js","directives/hydra-checks-footer.js","directives/footer.js","directives/focus-on.js","directives/downloaderStatusFooter.js","directives/download-nzbzip-button.js","directives/download-nzbs-button.js","directives/dataTableDirectives.js","directives/connection-test.js","directives/click-outside.js","directives/cfg-form-entry.js","directives/backup.js","directives/addable-nzbs.js","directives/addable-nzb.js","config/formly-indexers.js","config/formly-downloaders.js","config/formly-config.js","config/config-service.js","config/config-fields-service.js","config/config-controller.js","update-service.js","system-controller.js","stats-service.js","stats-controller.js","search-service.js","search-results-controller.js","search-history-service.js","search-history-controller.js","search-controller.js","restart-service.js","nzbhydra-control-service.js","nzb-download-service.js","notifications-service.js","notification-history-controller.js","modal.js","modal-service.js","migration-service.js","login-controller.js","indexer-statuses-controller.js","index-controller.js","hydra-auth-service.js","header-controller.js","generic-storage-service.js","generic-error-handler.js","filters.js","file-selection-service.js","file-download-service.js","downloader-categories-service.js","download-history-controller.js","debug-service.js","categories-service.js","backup-service.js","angular-scroll.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/5BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/LA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChWA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACdA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtXA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5HA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACloCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7ZA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxlBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/wEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/NA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9VA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1/BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC1NA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACheA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3HA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC/GA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACvFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACfA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"nzbhydra.js","sourcesContent":["// For caching HTML templates, see http://paulsalaets.com/pre-caching-angular-templates-with-gulp\nangular.module('templates', []);\n\nvar nzbhydraapp = angular.module('nzbhydraApp', ['angular-loading-bar', 'cgBusy', 'ui.bootstrap', 'ipCookie', 'angular-growl',\n 'angular.filter', 'filters', 'ui.router', 'blockUI', 'mgcrea.ngStrap', 'angularUtils.directives.dirPagination',\n 'nvd3', 'formly', 'formlyBootstrap', 'frapontillo.bootstrap-switch', 'ui.select', 'ngSanitize', 'checklist-model',\n 'ngAria', 'ngMessages', 'ui.router.title', 'LocalStorageModule', 'angular.filter', 'ngFileUpload', 'ngCookies', 'angular.chips',\n 'templates', 'base64', 'duScroll', 'colorpicker.module']);\n\nnzbhydraapp.config(['$compileProvider', function ($compileProvider) {\n $compileProvider.debugInfoEnabled(true);\n}]);\n\nnzbhydraapp.config(['$animateProvider', function ($animateProvider) {\n}]);\n\nangular.module('nzbhydraApp').config([\"$stateProvider\", \"$urlRouterProvider\", \"$locationProvider\", \"blockUIConfig\", \"$urlMatcherFactoryProvider\", \"localStorageServiceProvider\", \"bootstrapped\", function ($stateProvider, $urlRouterProvider, $locationProvider, blockUIConfig, $urlMatcherFactoryProvider, localStorageServiceProvider, bootstrapped) {\n blockUIConfig.autoBlock = false;\n blockUIConfig.resetOnException = false;\n blockUIConfig.autoInjectBodyBlock = false;\n $urlMatcherFactoryProvider.strictMode(false);\n\n $urlRouterProvider.otherwise(\"/\");\n\n $stateProvider\n .state('root', {\n url: '',\n abstract: true,\n resolve: {\n //loginRequired: loginRequired\n },\n views: {\n 'header': {\n templateUrl: 'static/html/states/header.html',\n controller: 'HeaderController',\n resolve: {\n bootstrapped: function () {\n return bootstrapped;\n }\n }\n }\n }\n })\n .state(\"root.config\", {\n url: \"/config\",\n views: {},\n abstract: true\n })\n .state(\"root.config.main\", {\n url: \"/main\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n controllerAs: 'ctrl',\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 0;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Main)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.auth\", {\n url: \"/auth\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 1;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Auth)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.searching\", {\n url: \"/searching\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 2;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Searching)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.categories\", {\n url: \"/categories\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 3;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Categories)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.downloading\", {\n url: \"/downloading\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 4;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Downloading)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.indexers\", {\n url: \"/indexers\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 5;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.config.notifications\", {\n url: \"/notifications\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/config.html\",\n controller: \"ConfigController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.get();\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 6;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Config (Notifications)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats\", {\n url: \"/stats\",\n abstract: true,\n views: {\n 'container@': {\n templateUrl: \"static/html/states/stats.html\",\n controller: [\"$scope\", \"$state\", function ($scope, $state) {\n $scope.$state = $state;\n $scope.bootstrapped = bootstrapped;\n }],\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n\n }\n }\n })\n .state(\"root.stats.main\", {\n url: \"/stats\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/main-stats.html\",\n controller: \"StatsController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.indexers\", {\n url: \"/indexers\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/indexer-statuses.html\",\n controller: IndexerStatusesController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n statuses: [\"$http\", function ($http) {\n return $http.get(\"internalapi/indexerstatuses\").then(function (response) {\n return response;\n });\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Indexers)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.searches\", {\n url: \"/searches\",\n views: {\n 'stats@root.stats': {\n templateUrl: \"static/html/states/search-history.html\",\n controller: SearchHistoryController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n history: ['loginRequired', 'SearchHistoryService', function (loginRequired, SearchHistoryService) {\n return SearchHistoryService.getSearchHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Searches)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.downloads\", {\n url: \"/downloads\",\n views: {\n 'stats@root.stats': {\n templateUrl: 'static/html/states/download-history.html',\n controller: DownloadHistoryController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n downloads: [\"StatsService\", function (StatsService) {\n return StatsService.getDownloadHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Downloads)\"\n }]\n }\n }\n }\n })\n .state(\"root.stats.notifications\", {\n url: \"/notifications\",\n views: {\n 'stats@root.stats': {\n templateUrl: 'static/html/states/notification-history.html',\n controller: NotificationHistoryController,\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"stats\")\n }],\n preloadData: [\"StatsService\", function (StatsService) {\n return StatsService.getNotificationHistory();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Stats (Notifications)\"\n }]\n }\n }\n }\n })\n .state(\"root.system\", {\n url: \"/system\",\n views: {},\n abstract: true\n })\n .state(\"root.system.control\", {\n url: \"/control\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 0;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System\"\n }]\n }\n }\n }\n })\n .state(\"root.system.updates\", {\n url: \"/updates\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 1;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Updates)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.log\", {\n url: \"/log\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 2;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Log)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.tasks\", {\n url: \"/tasks\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 3;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Tasks)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.backup\", {\n url: \"/backup\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 4;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Backup)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.bugreport\", {\n url: \"/bugreport\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 5;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (Bug report)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.news\", {\n url: \"/news\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n activeTab: [function () {\n return 6;\n }],\n simpleInfos: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (News)\"\n }]\n }\n }\n }\n })\n .state(\"root.system.about\", {\n url: \"/about\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/system.html\",\n controller: \"SystemController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"admin\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n simpleInfos: ['$http', 'RequestsErrorHandler', function ($http, RequestsErrorHandler) {\n return RequestsErrorHandler.specificallyHandled(function () {\n return $http.get(\"internalapi/updates/simpleInfos\").then(\n function (response) {\n return response.data;\n }\n );\n });\n }],\n activeTab: [function () {\n return 7;\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"System (About)\"\n }]\n }\n }\n }\n })\n\n .state(\"root.search\", {\n url: \"/?category&query&imdbId&tvdbId&title&season&episode&minsize&maxsize&minage&maxage&offsets&tvrageId&mode&tmdbId&indexers&tvmazeId&sortby&sortdirection\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/search.html\",\n controller: \"SearchController\",\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\n }],\n safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {\n return ConfigService.getSafe();\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Search\";\n }]\n }\n }\n }\n })\n .state(\"root.search.results\", {\n views: {\n 'results@root.search': {\n templateUrl: \"static/html/states/search-results.html\",\n controller: \"SearchResultsController\",\n controllerAs: \"srController\",\n options: {\n inherit: true\n },\n params: {\n modalInstance: null\n },\n resolve: {\n loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {\n return loginRequired($q, $timeout, $state, HydraAuthService, \"search\")\n }],\n $title: [\"$stateParams\", function ($stateParams) {\n var title = \"Search results\";\n var details;\n if ($stateParams.title) {\n details = $stateParams.title;\n } else if ($stateParams.query) {\n details = $stateParams.query;\n }\n if (details) {\n title += \" (\" + details + \")\";\n }\n return title;\n }]\n }\n }\n }\n })\n .state(\"root.login\", {\n url: \"/login\",\n views: {\n 'container@': {\n templateUrl: \"static/html/states/login.html\",\n controller: \"LoginController\",\n resolve: {\n loginRequired: function () {\n return null;\n },\n $title: [\"$stateParams\", function ($stateParams) {\n return \"Login\"\n }]\n }\n }\n }\n })\n ;\n\n\n $locationProvider.html5Mode(true);\n\n\n function loginRequired($q, $timeout, $state, HydraAuthService, type) {\n var deferred = $q.defer();\n var userInfos = HydraAuthService.getUserInfos();\n var allowed = false;\n if (type === \"search\") {\n allowed = !userInfos.searchRestricted || userInfos.maySeeSearch;\n } else if (type === \"stats\") {\n allowed = !userInfos.statsRestricted || userInfos.maySeeStats;\n } else if (type === \"admin\") {\n allowed = !userInfos.adminRestricted || userInfos.maySeeAdmin;\n } else {\n allowed = true;\n }\n if (allowed || userInfos.authType !== \"FORM\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n\n //Because I don't know for what state the login is required / asked I have a function for each\n\n function loginRequiredSearch($q, $timeout, $state, HydraAuthService) {\n var deferred = $q.defer();\n var userInfos = HydraAuthService.getUserInfos();\n if (!userInfos.searchRestricted || userInfos.maySeeSearch || userInfos.authType !== \"FORM\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n function loginRequiredStats($q, $timeout, $state, HydraAuthService) {\n var deferred = $q.defer();\n\n var userInfos = HydraAuthService.getUserInfos();\n if (!userInfos.statsRestricted || userInfos.maySeeStats || userInfos.authType !== \"FORM\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n function loginRequiredAdmin($q, $timeout, $state, HydraAuthService) {\n var deferred = $q.defer();\n\n var userInfos = HydraAuthService.getUserInfos();\n if (!userInfos.statsRestricted || userInfos.maySeeAdmin || userInfos.authType != \"form\") {\n deferred.resolve();\n } else {\n $timeout(function () {\n // This code runs after the authentication promise has been rejected.\n // Go to the log-in page\n $state.go(\"root.login\");\n })\n }\n return deferred.promise;\n }\n\n localStorageServiceProvider\n .setPrefix('nzbhydra');\n localStorageServiceProvider\n .setNotify(true, false);\n}]);\n\n\nnzbhydraapp.config([\"paginationTemplateProvider\", function (paginationTemplateProvider) {\n paginationTemplateProvider.setPath('static/html/dirPagination.tpl.html');\n}]);\n\nnzbhydraapp.config(['cfpLoadingBarProvider', function (cfpLoadingBarProvider) {\n cfpLoadingBarProvider.latencyThreshold = 100;\n}]);\n\nnzbhydraapp.config(['growlProvider', function (growlProvider) {\n growlProvider.globalTimeToLive(5000);\n growlProvider.globalPosition('bottom-right');\n}]);\n\nnzbhydraapp.directive('ngEnter', function () {\n return function (scope, element, attr) {\n element.bind(\"keydown keypress\", function (event) {\n if (event.which === 13) {\n scope.$apply(function () {\n scope.$evalAsync(attr.ngEnter);\n });\n\n event.preventDefault();\n }\n });\n };\n});\n\nnzbhydraapp.filter('nzblink', function () {\n return function (resultItem) {\n var uri = new URI(\"internalapi/getnzb/user/\" + resultItem.searchResultId);\n return uri.toString();\n }\n});\n\nnzbhydraapp.factory('focus', [\"$rootScope\", \"$timeout\", function ($rootScope, $timeout) {\n return function (name) {\n $timeout(function () {\n $rootScope.$broadcast('focusOn', name);\n });\n }\n}]);\n\nnzbhydraapp.run([\"$rootScope\", function ($rootScope) {\n $rootScope.$on('$stateChangeSuccess',\n function (event, toState, toParams, fromState, fromParams) {\n try {\n $rootScope.title = toState.views[Object.keys(toState.views)[0]].resolve.$title[1](toParams);\n } catch (e) {\n\n }\n\n });\n}]);\n\nnzbhydraapp.filter('dereferer', [\"ConfigService\", function (ConfigService) {\n return function (url) {\n if (ConfigService.getSafe().dereferer) {\n return ConfigService.getSafe().dereferer\n .replace(\"$s\", escape(url))\n .replace(\"$us\", url);\n }\n return url;\n }\n}]);\n\nnzbhydraapp.filter('derefererExtracting', [\"ConfigService\", function (ConfigService) {\n return function (aString) {\n if (!ConfigService.getSafe().dereferer || !aString) {\n return aString\n }\n var matches = aString.match(/(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?/);\n if (matches === null) {\n return aString;\n }\n\n aString = aString\n .replace(matches[0], ConfigService.getSafe().dereferer.replace(\"$s\", escape(matches[0])))\n .replace(matches[0], ConfigService.getSafe().dereferer.replace(\"$us\", matches[0]))\n ;\n\n return aString;\n }\n}]);\n\nnzbhydraapp.filter('binsearch', [\"ConfigService\", function (ConfigService) {\n return function (url) {\n return \"http://binsearch.info/?q=\" + encodeURIComponent(url) + \"&max=100&adv_age=3000&server=\";\n }\n}]);\n\nnzbhydraapp.config([\"$provide\", function ($provide) {\n $provide.decorator(\"$exceptionHandler\", ['$delegate', '$injector', function ($delegate, $injector) {\n return function (exception, cause) {\n $delegate(exception, cause);\n try {\n\n if (angular.isDefined(exception.stack)) {\n var stack = exception.stack.split('\\n').map(function (line) {\n return line.trim();\n });\n stack = stack.join(\"\\n\");\n //$injector.get(\"$http\").put(\"internalapi/logerror\", {error: stack, cause: angular.isDefined(cause) ? cause.toString() : \"No known cause\"});\n }\n } catch (e) {\n console.error(\"Unable to log JS exception to server\", e);\n }\n };\n }]);\n}]);\n\n_.mixin({\n isNullOrEmpty: function (string) {\n return (_.isUndefined(string) || _.isNull(string) || (_.isString(string) && string.length === 0))\n }\n});\n\nnzbhydraapp.factory('sessionInjector', [\"$injector\", function ($injector) {\n var sessionInjector = {\n response: function (response) {\n if (response.headers(\"Hydra-MaySeeAdmin\") != null) {\n $injector.get(\"HydraAuthService\").setLoggedInByBasic(response.headers(\"Hydra-MaySeeStats\") == \"True\", response.headers(\"Hydra-MaySeeAdmin\") == \"True\", response.headers(\"Hydra-Username\"))\n }\n\n return response;\n }\n };\n return sessionInjector;\n}]);\n\nnzbhydraapp.config(['$httpProvider', function ($httpProvider) {\n $httpProvider.interceptors.push('sessionInjector');\n $httpProvider.defaults.xsrfCookieName = 'HYDRA-XSRF-TOKEN';\n}]);\n\nnzbhydraapp.directive('autoFocus', [\"$timeout\", function ($timeout) {\n return {\n restrict: 'AC',\n link: function (_scope, _element, attrs) {\n if (attrs.noFocus) {\n return;\n }\n $timeout(function () {\n _element[0].focus();\n }, 0);\n }\n };\n}]);\n\nnzbhydraapp.factory('responseObserver', [\"$q\", \"$window\", \"growl\", function responseObserver($q, $window, growl) {\n return {\n 'responseError': function (errorResponse) {\n switch (errorResponse.status) {\n case 403:\n growl.info(\"You are not allowed to visit that section.\");\n break;\n }\n if (angular.isDefined(errorResponse.config)) {\n errorResponse.config.alreadyHandled = true;\n }\n return $q.reject(errorResponse);\n }\n };\n}]);\n\nnzbhydraapp.config([\"$httpProvider\", function ($httpProvider) {\n $httpProvider.interceptors.push('responseObserver');\n}]);\n\n\nnzbhydraapp.factory('focus', [\"$timeout\", \"$window\", function ($timeout, $window) {\n return function (id) {\n // timeout makes sure that it is invoked after any other event has been triggered.\n // e.g. click events that need to run before the focus or\n // inputs elements that are in a disabled state but are enabled when those events\n // are triggered.\n $timeout(function () {\n var element = $window.document.getElementById(id);\n if (element)\n element.focus();\n });\n };\n}]);\n\nnzbhydraapp.directive('eventFocus', [\"focus\", function (focus) {\n return function (scope, elem, attr) {\n elem.on(attr.eventFocus, function () {\n focus(attr.eventFocusId);\n });\n\n // Removes bound events in the element itself\n // when the scope is destroyed\n scope.$on('$destroy', function () {\n elem.off(attr.eventFocus);\n });\n };\n}]);\n\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nangular\n .module('nzbhydraApp')\n .directive('hydraTasks', hydraTasks);\n\nfunction hydraTasks() {\n controller.$inject = [\"$scope\", \"$http\"];\n return {\n templateUrl: 'static/html/directives/tasks.html',\n controller: controller\n };\n\n function controller($scope, $http) {\n\n $http.get(\"internalapi/tasks\").then(function (response) {\n $scope.tasks = response.data;\n });\n\n $scope.runTask = function (taskName) {\n $http.put(\"internalapi/tasks/\" + taskName).then(function (response) {\n $scope.tasks = response.data;\n });\n }\n }\n}\n\n","angular\r\n .module('nzbhydraApp')\r\n .directive('tabOrChart', tabOrChart);\r\n\r\nfunction tabOrChart() {\r\n return {\r\n templateUrl: 'static/html/directives/tab-or-chart.html',\r\n transclude: {\r\n \"chartSlot\": \"chart\",\r\n \"tableSlot\": \"table\"\r\n },\r\n restrict: 'E',\r\n replace: true,\r\n scope: {\r\n display: \"@\"\r\n }\r\n\r\n };\r\n\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('selectionButton', selectionButton);\r\n\r\nfunction selectionButton() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/selection-button.html',\r\n scope: {\r\n selected: \"=\",\r\n selectable: \"=\",\r\n invertSelection: \"<\",\r\n selectAll: \"<\",\r\n deselectAll: \"<\",\r\n btn: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n if (angular.isUndefined($scope.btn)) {\r\n $scope.btn = \"default\"; //Will form class \"btn-default\"\r\n }\r\n\r\n if (angular.isUndefined($scope.invertSelection)) {\r\n $scope.invertSelection = function () {\r\n $scope.selected = _.difference($scope.selectable, $scope.selected);\r\n };\r\n }\r\n\r\n if (angular.isUndefined($scope.selectAll)) {\r\n $scope.selectAll = function () {\r\n $scope.selected.push.apply($scope.selected, $scope.selectable);\r\n };\r\n }\r\n\r\n if (angular.isUndefined($scope.deselectAll)) {\r\n $scope.deselectAll = function () {\r\n $scope.selected.splice(0, $scope.selected.length);\r\n };\r\n }\r\n\r\n\r\n }\r\n}\r\n\r\n","\nNfoModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"nfo\"];angular\n .module('nzbhydraApp')\n .directive('searchResult', searchResult);\n\nfunction searchResult() {\n controller.$inject = [\"$scope\", \"$element\", \"$http\", \"growl\", \"$attrs\", \"$uibModal\", \"$window\", \"DebugService\", \"localStorageService\", \"HydraAuthService\", \"ConfigService\"];\n return {\n templateUrl: 'static/html/directives/search-result.html',\n require: '^result',\n replace: false,\n scope: {\n result: \"<\",\n searchResultsControllerShared: \"<\"\n },\n controller: controller\n };\n\n\n function handleDisplay($scope, localStorageService, ConfigService) {\n //Display state / expansion\n $scope.foo.duplicatesDisplayed = localStorageService.get(\"duplicatesDisplayed\") !== null ? localStorageService.get(\"duplicatesDisplayed\") : false;\n $scope.foo.showCovers = localStorageService.get(\"showCovers\") !== null ? localStorageService.get(\"showCovers\") : true;\n $scope.foo.alwaysShowTitles = localStorageService.get(\"alwaysShowTitles\") !== null ? localStorageService.get(\"alwaysShowTitles\") : true;\n $scope.duplicatesExpanded = false;\n $scope.titlesExpanded = $scope.searchResultsControllerShared.expandGroupsByDefault;\n $scope.coverSize = ConfigService.getSafe().searching.coverSize;\n\n function calculateDisplayState() {\n $scope.resultDisplayed = ($scope.result.titleGroupIndex === 0 || $scope.titlesExpanded) && ($scope.duplicatesExpanded || $scope.result.duplicateGroupIndex === 0);\n }\n\n calculateDisplayState();\n\n $scope.toggleTitleExpansion = function () {\n $scope.titlesExpanded = !$scope.titlesExpanded;\n $scope.$emit(\"toggleTitleExpansionUp\", $scope.titlesExpanded, $scope.result.titleGroupIndicator);\n };\n\n $scope.toggleDuplicateExpansion = function () {\n $scope.duplicatesExpanded = !$scope.duplicatesExpanded;\n $scope.$emit(\"toggleDuplicateExpansionUp\", $scope.duplicatesExpanded, $scope.result.hash);\n };\n\n $scope.$on(\"toggleTitleExpansionDown\", function ($event, value, titleGroupIndicator) {\n if ($scope.result.titleGroupIndicator === titleGroupIndicator) {\n $scope.titlesExpanded = value;\n calculateDisplayState();\n }\n });\n\n $scope.$on(\"toggleDuplicateExpansionDown\", function ($event, value, hash) {\n if ($scope.result.hash === hash) {\n $scope.duplicatesExpanded = value;\n calculateDisplayState();\n }\n });\n\n $scope.$on(\"toggleShowCovers\", function ($event, value) {\n $scope.foo.showCovers = value;\n });\n\n $scope.$on(\"toggleAlwaysShowTitles\", function ($event, value) {\n $scope.foo.alwaysShowTitles = value;\n console.log(\"alwaysShowTitles: \" + alwaysShowTitles);\n });\n\n $scope.$on(\"duplicatesDisplayed\", function ($event, value) {\n $scope.foo.duplicatesDisplayed = value;\n if (!value) {\n //Collapse duplicate groups they shouldn't be displayed\n $scope.duplicatesExpanded = false;\n }\n calculateDisplayState();\n });\n\n $scope.$on(\"calculateDisplayState\", function () {\n calculateDisplayState();\n });\n }\n\n function handleSelection($scope, $element) {\n $scope.foo.selected = false;\n\n function sendSelectionEvent(isSelected) {\n $scope.$emit(\"selectionUp\", $scope.result, isSelected);\n }\n\n $scope.clickCheckbox = function (event, result) {\n var isSelected = event.currentTarget.checked;\n sendSelectionEvent(isSelected);\n $scope.$emit(\"checkboxClicked\", event, $scope.rowIndex, isSelected, event.currentTarget);\n };\n\n function isBetween(num, betweena, betweenb) {\n return (betweena <= num && num <= betweenb) || (betweena >= num && num >= betweenb);\n }\n\n $scope.$on(\"shiftClick\", function (event, startIndex, endIndex, newValue, previousClickTargetElement, newClickTargetElement) {\n var fromYlocation = $($(previousClickTargetElement).prop(\"parentNode\")).prop(\"offsetTop\");\n var newYlocation = $($(newClickTargetElement).prop(\"parentNode\")).prop(\"offsetTop\");\n var elementYlocation = $($element).prop(\"offsetTop\");\n if (!$scope.resultDisplayed) {\n return;\n }\n\n if (isBetween(elementYlocation, fromYlocation, newYlocation)) {\n sendSelectionEvent(newValue);\n $scope.foo.selected = newValue === 1;\n }\n });\n\n $scope.$on(\"invertSelection\", function () {\n if (!$scope.resultDisplayed) {\n return;\n }\n $scope.foo.selected = !$scope.foo.selected;\n sendSelectionEvent($scope.foo.selected);\n });\n\n $scope.$on(\"deselectAll\", function () {\n if (!$scope.resultDisplayed) {\n return;\n }\n $scope.foo.selected = false;\n sendSelectionEvent($scope.foo.selected);\n });\n\n $scope.$on(\"selectAll\", function () {\n if (!$scope.resultDisplayed) {\n return;\n }\n $scope.foo.selected = true;\n\n sendSelectionEvent($scope.foo.selected);\n });\n\n $scope.$on(\"toggleSelection\", function ($event, result, value) {\n if (!$scope.resultDisplayed || result !== $scope.result) {\n return;\n }\n $scope.foo.selected = value;\n });\n }\n\n function handleNfoDisplay($scope, $http, growl, $uibModal, HydraAuthService) {\n $scope.showDetailsDl = HydraAuthService.getUserInfos().maySeeDetailsDl;\n\n $scope.showNfo = showNfo;\n\n function showNfo(resultItem) {\n if (resultItem.has_nfo === 0) {\n return;\n }\n var uri = new URI(\"internalapi/nfo/\" + resultItem.searchResultId);\n return $http.get(uri.toString()).then(function (response) {\n if (response.data.successful) {\n if (response.data.hasNfo) {\n $scope.openModal(\"lg\", response.data.content)\n } else {\n growl.info(\"No NFO available\");\n }\n } else {\n growl.error(response.data.content);\n }\n });\n }\n\n $scope.openModal = openModal;\n\n function openModal(size, nfo) {\n var modalInstance = $uibModal.open({\n template: '
          ',\n controller: NfoModalInstanceCtrl,\n size: size,\n resolve: {\n nfo: function () {\n return nfo;\n }\n }\n });\n\n modalInstance.result.then();\n }\n\n $scope.getNfoTooltip = function () {\n if ($scope.result.hasNfo === \"YES\") {\n return \"Show NFO\"\n } else if ($scope.result.hasNfo === \"MAYBE\") {\n return \"Try to load NFO (may not be available)\";\n } else {\n return \"No NFO available\";\n }\n };\n }\n\n function handleNzbDownload($scope, $window) {\n $scope.downloadNzb = downloadNzb;\n\n function downloadNzb(resultItem) {\n //href = \"{{ result.link }}\"\n $window.location.href = resultItem.link;\n }\n }\n\n\n function controller($scope, $element, $http, growl, $attrs, $uibModal, $window, DebugService, localStorageService, HydraAuthService, ConfigService) {\n $scope.foo = {};\n handleDisplay($scope, localStorageService, ConfigService);\n handleSelection($scope, $element);\n handleNfoDisplay($scope, $http, growl, $uibModal, HydraAuthService);\n handleNzbDownload($scope, $window);\n\n $scope.kify = function () {\n return function (number) {\n if (number > 1000) {\n return Math.round(number / 1000) + \"k\";\n }\n return number;\n };\n };\n\n\n $scope.showCover = function (url) {\n console.log(\"Show \" + url);\n $uibModal.open({\n template: '
          \\n' +\n ' \\n' +\n '
          ',\n controller: [\"$scope\", \"url\", function ($scope, url) {\n $scope.url = url;\n }],\n resolve: {\n url: function () {\n return url;\n }\n },\n size: \"md\",\n keyboard: true,\n windowTopClass: 'cover-modal-dialog'\n });\n };\n\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('NfoModalInstanceCtrl', NfoModalInstanceCtrl);\n\nfunction NfoModalInstanceCtrl($scope, $uibModalInstance, nfo) {\n\n $scope.nfo = nfo;\n\n $scope.ok = function () {\n $uibModalInstance.close($scope.selected.item);\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n}\n\nangular\n .module('nzbhydraApp')\n .filter('kify', function () {\n return function (number) {\n if (number > 1000) {\n return Math.round(number / 1000) + \"k\";\n }\n return number;\n }\n });\n","angular\r\n .module('nzbhydraApp')\r\n .directive('saveOrSendFile', saveOrSendFile);\r\n\r\nfunction saveOrSendFile() {\r\n controller.$inject = [\"$scope\", \"$http\", \"growl\", \"ConfigService\"];\r\n return {\r\n templateUrl: 'static/html/directives/save-or-send-file.html',\r\n scope: {\r\n searchResultId: \"<\",\r\n isFile: \"<\",\r\n type: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, growl, ConfigService) {\r\n $scope.cssClass = \"glyphicon-save-file\";\r\n var endpoint;\r\n if ($scope.type === \"TORRENT\") {\r\n $scope.enableButton = !_.isNullOrEmpty(ConfigService.getSafe().downloading.saveTorrentsTo) || ConfigService.getSafe().downloading.sendMagnetLinks;\r\n $scope.tooltip = \"Save torrent to black hole or send magnet link\";\r\n endpoint = \"internalapi/saveOrSendTorrent\";\r\n } else {\r\n $scope.tooltip = \"Save NZB to black hole\";\r\n $scope.enableButton = !_.isNullOrEmpty(ConfigService.getSafe().downloading.saveNzbsTo);\r\n endpoint = \"internalapi/saveNzbToBlackhole\";\r\n }\r\n $scope.add = function () {\r\n $scope.cssClass = \"nzb-spinning\";\r\n $http.put(endpoint, $scope.searchResultId).then(function (response) {\r\n if (response.data.successful) {\r\n $scope.cssClass = \"glyphicon-ok\";\r\n } else {\r\n $scope.cssClass = \"glyphicon-remove\";\r\n growl.error(response.data.message);\r\n }\r\n });\r\n };\r\n }\r\n}\r\n","//Can be used in an ng-repeat directive to call a function when the last element was rendered\n//We use it to mark the end of sorting / filtering so we can stop blocking the UI\n\nonFinishRender.$inject = [\"$timeout\"];\nangular\n .module('nzbhydraApp')\n .directive('onFinishRender', onFinishRender);\n\nfunction onFinishRender($timeout) {\n function linkFunction(scope, element, attr) {\n\n if (scope.$last === true) {\n console.log(\"Render finished\");\n // console.timeEnd(\"Presenting\");\n // console.timeEnd(\"searchall\");\n scope.$emit(\"onFinishRender\")\n }\n }\n\n return {\n link: linkFunction\n }\n}","//Fork of https://github.com/dotansimha/angularjs-dropdown-multiselect to make it compatible with formly\nangular\n .module('nzbhydraApp')\n .directive('multiselectDropdown',\n\n dropdownMultiselectDirective\n );\n\nfunction dropdownMultiselectDirective() {\n return {\n scope: {\n selectedModel: '=',\n options: '=',\n settings: '=?',\n events: '=?'\n },\n transclude: {\n toggleDropdown: '?toggleDropdown'\n },\n templateUrl: 'static/html/directives/multiselect-dropdown.html',\n controller: [\"$scope\", \"$element\", \"$filter\", \"$document\", function dropdownMultiselectController($scope, $element, $filter, $document) {\n var $dropdownTrigger = $element.children()[0];\n\n var settings = {\n showSelectedValues: true,\n showSelectAll: true,\n showDeselectAll: true,\n noSelectedText: 'None selected'\n };\n var events = {\n onToggleItem: angular.noop\n };\n angular.extend(events, $scope.events || []);\n angular.extend(settings, $scope.settings || []);\n angular.extend($scope, {settings: settings, events: events});\n\n $scope.buttonText = \"\";\n if (settings.buttonText) {\n $scope.buttonText = settings.buttonText;\n } else {\n $scope.$watch(\"selectedModel\", function () {\n if (angular.isDefined($scope.selectedModel) && settings.showSelectedValues) {\n if ($scope.selectedModel.length === 0) {\n if ($scope.settings.noSelectedText) {\n $scope.buttonText = $scope.settings.noSelectedText;\n } else {\n $scope.buttonText = \"None selected\";\n }\n } else if ($scope.selectedModel.length === $scope.options.length) {\n $scope.buttonText = \"All selected\";\n } else {\n var selected = [];\n _.each($scope.options, function (x) {\n if ($scope.selectedModel.indexOf(x.id) > -1) {\n selected.push(x.label);\n }\n })\n $scope.buttonText = selected.join(\", \");\n }\n } else {\n if (angular.isUndefined($scope.selectedModel) || ($scope.settings.noSelectedText && $scope.selectedModel.length === 0)) {\n $scope.buttonText = $scope.settings.noSelectedText;\n } else {\n $scope.buttonText = $scope.selectedModel.length + \" / \" + $scope.options.length + \" selected\";\n }\n }\n }, true);\n }\n $scope.open = false;\n\n $scope.toggleDropdown = function () {\n $scope.open = !$scope.open;\n };\n\n $scope.toggleItem = function (option) {\n var index = $scope.selectedModel.indexOf(option.id);\n var oldValue = index > -1;\n if (oldValue) {\n $scope.selectedModel.splice(index, 1);\n } else {\n $scope.selectedModel.push(option.id);\n }\n $scope.events.onToggleItem(option, !oldValue);\n };\n\n $scope.selectAll = function () {\n $scope.selectedModel = _.pluck($scope.options, \"id\");\n };\n\n $scope.deselectAll = function () {\n $scope.selectedModel.splice(0, $scope.selectedModel.length);\n };\n\n //Close when clicked outside\n\n $document.on('click', function (e) {\n function contains(collection, target) {\n var containsTarget = false;\n collection.some(function (object) {\n if (object === target) {\n containsTarget = true;\n return true;\n }\n return false;\n });\n return containsTarget;\n }\n\n if ($scope.open) {\n var target = e.target.parentElement;\n var parentFound = false;\n\n while (angular.isDefined(target) && target !== null && !parentFound) {\n if (!!target.className.split && contains(target.className.split(' '), 'multiselect-parent') && !parentFound) {\n if (target === $dropdownTrigger) {\n parentFound = true;\n }\n }\n target = target.parentElement;\n }\n\n if (!parentFound) {\n $scope.$apply(function () {\n $scope.open = false;\n });\n }\n }\n });\n\n\n }]\n\n }\n}","angular\r\n .module('nzbhydraApp').directive(\"keepFocus\", ['$timeout', function ($timeout) {\r\n /*\r\n Intended use:\r\n \r\n */\r\n return {\r\n restrict: 'A',\r\n require: 'ngModel',\r\n link: function ($scope, $element, attrs, ngModel) {\r\n\r\n ngModel.$parsers.unshift(function (value) {\r\n $timeout(function () {\r\n $element[0].focus();\r\n });\r\n return value;\r\n });\r\n\r\n }\r\n };\r\n}]);","/*\r\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .directive('indexerStateSwitch', indexerStateSwitch);\r\n\r\nfunction indexerStateSwitch() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-state-switch.html',\r\n scope: {\r\n indexer: \"=\",\r\n handleWidth: \"@\"\r\n },\r\n replace: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.value = $scope.indexer.state === \"ENABLED\";\r\n $scope.handleWidth = $scope.handleWidth || \"130px\";\r\n var initialized = false;\r\n\r\n function calculateTextAndColor() {\r\n if ($scope.indexer.state === \"DISABLED_USER\") {\r\n $scope.offText = \"Disabled by user\";\r\n $scope.offColor = \"default\";\r\n } else if ($scope.indexer.state === \"DISABLED_SYSTEM_TEMPORARY\") {\r\n $scope.offText = \"Temporary disabled\";\r\n $scope.offColor = \"warning\";\r\n } else if ($scope.indexer.state === \"DISABLED_SYSTEM\") {\r\n $scope.offText = \"Disabled by system\";\r\n $scope.offColor = \"danger\";\r\n }\r\n }\r\n\r\n calculateTextAndColor();\r\n\r\n $scope.onChange = function () {\r\n if (initialized) {\r\n //Skip on first call when initial value is set\r\n $scope.indexer.state = $scope.value ? \"ENABLED\" : \"DISABLED_USER\";\r\n calculateTextAndColor();\r\n }\r\n initialized = true;\r\n }\r\n }\r\n}","/*\r\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .directive('indexerSelectionButton', indexerSelectionButton);\r\n\r\nfunction indexerSelectionButton() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-selection-button.html',\r\n scope: {\r\n selectedIndexers: \"=\",\r\n availableIndexers: \"=\",\r\n btn: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n\r\n $scope.anyTorrentIndexersSelectable = _.any($scope.availableIndexers,\r\n function (indexer) {\r\n return indexer.searchModuleType === \"TORZNAB\";\r\n }\r\n );\r\n\r\n $scope.invertSelection = function () {\r\n _.forEach($scope.availableIndexers, function (x) {\r\n var index = _.indexOf($scope.selectedIndexers, x.name);\r\n if (index === -1) {\r\n $scope.selectedIndexers.push(x.name);\r\n } else {\r\n $scope.selectedIndexers.splice(index, 1);\r\n }\r\n });\r\n };\r\n\r\n $scope.selectAll = function () {\r\n $scope.deselectAll();\r\n $scope.selectedIndexers.push.apply($scope.selectedIndexers, _.pluck($scope.availableIndexers, \"name\"));\r\n };\r\n\r\n $scope.deselectAll = function () {\r\n $scope.selectedIndexers.splice(0, $scope.selectedIndexers.length);\r\n };\r\n\r\n function selectByPredicate(predicate) {\r\n $scope.deselectAll();\r\n $scope.selectedIndexers.push.apply($scope.selectedIndexers,\r\n _.pluck(\r\n _.filter($scope.availableIndexers,\r\n predicate\r\n ), \"name\")\r\n );\r\n }\r\n\r\n $scope.reset = function () {\r\n selectByPredicate(function (indexer) {\r\n return indexer.preselect;\r\n });\r\n };\r\n\r\n $scope.selectAllUsenet = function () {\r\n selectByPredicate(function (indexer) {\r\n return indexer.searchModuleType !== \"TORZNAB\";\r\n });\r\n };\r\n\r\n $scope.selectAllTorrent = function () {\r\n selectByPredicate(function (indexer) {\r\n return indexer.searchModuleType === \"TORZNAB\";\r\n });\r\n }\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('indexerInput', indexerInput);\r\n\r\nfunction indexerInput() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/indexer-input.html',\r\n scope: {\r\n indexer: \"=\",\r\n model: \"=\",\r\n onClick: \"=\"\r\n },\r\n replace: true,\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.isFocused = false;\r\n\r\n $scope.onFocus = function () {\r\n $scope.isFocused = true;\r\n };\r\n\r\n $scope.onBlur = function () {\r\n $scope.isFocused = false;\r\n };\r\n\r\n var expiryWarning;\r\n if ($scope.indexer.vipExpirationDate != null && $scope.indexer.vipExpirationDate !== \"Lifetime\") {\r\n var expiryDate = moment($scope.indexer.vipExpirationDate, \"YYYY-MM-DD\");\r\n if (expiryDate < moment()) {\r\n console.log(\"Expiry date reached for indexer \" + $scope.indexer.name);\r\n expiryWarning = \"VIP access expired on \" + $scope.indexer.vipExpirationDate;\r\n } else if (expiryDate.subtract(7, 'days') < moment()) {\r\n console.log(\"Expiry date near for indexer \" + $scope.indexer.name);\r\n expiryWarning = \"VIP access will expire on \" + $scope.indexer.vipExpirationDate;\r\n }\r\n }\r\n\r\n $scope.expiryWarning = expiryWarning;\r\n if ($scope.indexer.color !== null) {\r\n $scope.style = \"background-color: \" + $scope.indexer.color.replace(\"rgb\", \"rgba\").replace(\")\", \",0.5)\")\r\n }\r\n }\r\n\r\n}\r\n\r\n","angular\n .module('nzbhydraApp')\n .directive('hydraupdates', hydraupdates);\n\nfunction hydraupdates() {\n controller.$inject = [\"$scope\", \"UpdateService\"];\n return {\n templateUrl: 'static/html/directives/updates.html',\n controller: controller\n };\n\n function controller($scope, UpdateService) {\n\n $scope.loadingPromise = UpdateService.getInfos().then(function (response) {\n $scope.currentVersion = response.data.currentVersion;\n $scope.latestVersion = response.data.latestVersion;\n $scope.latestVersionIsBeta = response.data.latestVersionIsBeta;\n $scope.betaVersion = response.data.betaVersion;\n $scope.updateAvailable = response.data.updateAvailable;\n $scope.betaUpdateAvailable = response.data.betaUpdateAvailable;\n $scope.latestVersionIgnored = response.data.latestVersionIgnored;\n $scope.changelog = response.data.changelog;\n $scope.updatedExternally = response.data.updatedExternally;\n $scope.wrapperOutdated = response.data.wrapperOutdated;\n $scope.showUpdateBannerOnUpdatedExternally = response.data.showUpdateBannerOnUpdatedExternally;\n if ($scope.updatedExternally && !$scope.showUpdateBannerOnUpdatedExternally) {\n $scope.updateAvailable = false;\n }\n });\n\n UpdateService.getVersionHistory().then(function (response) {\n $scope.versionHistory = response.data;\n });\n\n\n $scope.update = function (version) {\n UpdateService.update(version);\n };\n\n $scope.showChangelog = function (version) {\n UpdateService.showChanges(version);\n };\n\n $scope.forceUpdate = function () {\n UpdateService.update($scope.latestVersion)\n };\n }\n}\n\n","angular\r\n .module('nzbhydraApp')\r\n .directive('hydraNews', hydraNews);\r\n\r\nfunction hydraNews() {\r\n controller.$inject = [\"$scope\", \"$http\"];\r\n return {\r\n templateUrl: \"static/html/directives/news.html\",\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http) {\r\n\r\n return $http.get(\"internalapi/news\").then(function (response) {\r\n $scope.news = response.data;\r\n });\r\n\r\n\r\n }\r\n}\r\n\r\n","\r\nLogModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"entry\"];\r\nescapeHtml.$inject = [\"$sanitize\"];angular\r\n .module('nzbhydraApp')\r\n .directive('hydralog', hydralog);\r\n\r\nfunction hydralog() {\r\n controller.$inject = [\"$scope\", \"$http\", \"$interval\", \"$uibModal\", \"$sce\", \"localStorageService\", \"growl\"];\r\n return {\r\n templateUrl: \"static/html/directives/log.html\",\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, $interval, $uibModal, $sce, localStorageService, growl) {\r\n $scope.tailInterval = null;\r\n $scope.doUpdateLog = localStorageService.get(\"doUpdateLog\") !== null ? localStorageService.get(\"doUpdateLog\") : false;\r\n $scope.doTailLog = localStorageService.get(\"doTailLog\") !== null ? localStorageService.get(\"doTailLog\") : false;\r\n\r\n $scope.active = 0;\r\n $scope.currentJsonIndex = 0;\r\n $scope.hasMoreJsonLines = true;\r\n\r\n function getLog(index) {\r\n if ($scope.active === 0) {\r\n return $http.get(\"internalapi/debuginfos/jsonlogs\", {\r\n params: {\r\n offset: index,\r\n limit: 500\r\n }\r\n }).then(function (response) {\r\n var data = response.data;\r\n $scope.jsonLogLines = angular.fromJson(data.lines);\r\n $scope.hasMoreJsonLines = data.hasMore;\r\n });\r\n } else if ($scope.active === 1) {\r\n return $http.get(\"internalapi/debuginfos/currentlogfile\").then(function (response) {\r\n var data = response.data;\r\n $scope.log = $sce.trustAsHtml(data.replace(/&/g, \"&\")\r\n .replace(//g, \">\")\r\n .replace(/\"/g, \""\")\r\n .replace(/'/g, \"'\"));\r\n }, function (data) {\r\n growl.error(data)\r\n });\r\n } else if ($scope.active === 2) {\r\n return $http.get(\"internalapi/debuginfos/logfilenames\").then(function (response) {\r\n $scope.logfilenames = response.data;\r\n });\r\n }\r\n }\r\n\r\n $scope.logPromise = getLog();\r\n\r\n $scope.select = function (index) {\r\n $scope.active = index;\r\n $scope.update();\r\n };\r\n\r\n $scope.scrollToBottom = function () {\r\n document.getElementById(\"logfile\").scrollTop = 10000000;\r\n document.getElementById(\"logfile\").scrollTop = 100001000;\r\n };\r\n\r\n $scope.update = function () {\r\n getLog($scope.currentJsonIndex);\r\n if ($scope.active === 1) {\r\n $scope.scrollToBottom();\r\n }\r\n };\r\n\r\n $scope.getOlderFormatted = function () {\r\n getLog($scope.currentJsonIndex + 500).then(function () {\r\n $scope.currentJsonIndex += 500;\r\n });\r\n\r\n };\r\n\r\n $scope.getNewerFormatted = function () {\r\n var index = Math.max($scope.currentJsonIndex - 500, 0);\r\n getLog(index);\r\n $scope.currentJsonIndex = index;\r\n };\r\n\r\n function startUpdateLogInterval() {\r\n $scope.tailInterval = $interval(function () {\r\n if ($scope.active === 1) {\r\n $scope.update();\r\n if ($scope.doTailLog && $scope.active === 1) {\r\n $scope.scrollToBottom();\r\n }\r\n }\r\n }, 5000);\r\n }\r\n\r\n $scope.toggleUpdate = function (doUpdateLog) {\r\n $scope.doUpdateLog = doUpdateLog;\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n } else if ($scope.tailInterval !== null) {\r\n console.log(\"Cancelling\");\r\n $interval.cancel($scope.tailInterval);\r\n localStorageService.set(\"doTailLog\", false);\r\n $scope.doTailLog = false;\r\n }\r\n localStorageService.set(\"doUpdateLog\", $scope.doUpdateLog);\r\n };\r\n\r\n $scope.toggleTailLog = function () {\r\n localStorageService.set(\"doTailLog\", $scope.doTailLog);\r\n };\r\n\r\n $scope.openModal = function openModal(entry) {\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'log-entry.html',\r\n controller: LogModalInstanceCtrl,\r\n size: \"xl\",\r\n resolve: {\r\n entry: function () {\r\n return entry;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then();\r\n };\r\n\r\n $scope.$on('$destroy', function () {\r\n if ($scope.tailInterval !== null) {\r\n $interval.cancel($scope.tailInterval);\r\n }\r\n });\r\n\r\n if ($scope.doUpdateLog) {\r\n startUpdateLogInterval();\r\n }\r\n\r\n\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('LogModalInstanceCtrl', LogModalInstanceCtrl);\r\n\r\nfunction LogModalInstanceCtrl($scope, $uibModalInstance, entry) {\r\n\r\n $scope.entry = entry;\r\n\r\n $scope.ok = function () {\r\n $uibModalInstance.dismiss();\r\n };\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('formatTimestamp', formatTimestamp);\r\n\r\nfunction formatTimestamp() {\r\n return function (date) {\r\n //1579392000\r\n //1579374757\r\n if (date === null || date === undefined) {\r\n return null;\r\n }\r\n if (date < 1979374757) {\r\n date *= 1000;\r\n }\r\n return moment(date).local().format(\"YYYY-MM-DD HH:mm\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('escapeHtml', escapeHtml);\r\n\r\nfunction escapeHtml($sanitize) {\r\n return function (text) {\r\n return $sanitize(text);\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .filter('formatClassname', formatClassname);\r\n\r\nfunction formatClassname() {\r\n return function (fqn) {\r\n return fqn.substr(fqn.lastIndexOf(\".\") + 1);\r\n\r\n }\r\n}","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nNewsModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"news\"];\nWelcomeModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$state\", \"MigrationService\"];\nangular\n .module('nzbhydraApp')\n .directive('hydraChecksFooter', hydraChecksFooter);\n\nfunction hydraChecksFooter() {\n controller.$inject = [\"$scope\", \"UpdateService\", \"RequestsErrorHandler\", \"HydraAuthService\", \"$http\", \"$uibModal\", \"ConfigService\", \"GenericStorageService\", \"ModalService\", \"growl\", \"NotificationService\", \"bootstrapped\"];\n return {\n templateUrl: 'static/html/directives/checks-footer.html',\n controller: controller\n };\n\n function controller($scope, UpdateService, RequestsErrorHandler, HydraAuthService, $http, $uibModal, ConfigService, GenericStorageService, ModalService, growl, NotificationService, bootstrapped) {\n $scope.updateAvailable = false;\n $scope.checked = false;\n var welcomeIsBeingShown = false;\n\n $scope.mayUpdate = HydraAuthService.getUserInfos().maySeeAdmin;\n\n $scope.$on(\"user:loggedIn\", function () {\n if (HydraAuthService.getUserInfos().maySeeAdmin && !$scope.checked) {\n retrieveUpdateInfos();\n }\n });\n\n function checkForOutOfMemoryException() {\n GenericStorageService.get(\"outOfMemoryDetected\", false).then(function (response) {\n if (response.data !== \"\" && response.data) {\n //headline, message, params, size, textAlign\n ModalService.open(\"Out of memory error detected\", 'The log indicates that the process ran out of memory. Please increase the XMX value in the main config and restart.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"outOfMemoryDetected\", false, false);\n }\n });\n }\n\n function checkForOpenToInternet() {\n GenericStorageService.get(\"showOpenToInternetWithoutAuth\", false).then(function (response) {\n if (response.data !== \"\" && response.data) {\n //headline, message, params, size, textAlign\n ModalService.open(\"Security issue - open to internet\", 'It looks like NZBHydra is exposed to the internet without any authentication enable. Please make sure it cannot be reached from outside your network or enable an authentication method.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"showOpenToInternetWithoutAuth\", false, false);\n }\n });\n }\n\n console.log(\"Checking for below Java 17.\");\n\n function checkForJavaBelow17() {\n GenericStorageService.get(\"belowJava17\", false).then(function (response) {\n if (response.data !== \"\" && response.data) {\n console.log(\"Java below 17\");\n //headline, message, params, size, textAlign\n ModalService.open(\"Java version below 17\", 'You\\'re currently running NZBHydra2 with an older java version. A future update will require Java 17. Please install Java 17 (not higher) from here.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"belowJava17\", false, false);\n }\n });\n }\n\n console.log(\"Checking for failed backup.\");\n\n function checkForFailedBackup() {\n GenericStorageService.get(\"FAILED_BACKUP\", false).then(function (response) {\n if (response.data !== \"\" && response.data && !response.data) {\n console.log(\"Failed backup detected\");\n //headline, message, params, size, textAlign\n ModalService.open(\"Failed backup\", 'The creation of a backup file has failed. Error message: \\\"' + response.data.message + '.\"
          For details please check the log around ' + response.data.time + '.', {\n yes: {\n text: \"OK\"\n }\n }, undefined, \"left\");\n GenericStorageService.put(\"FAILED_BACKUP\", false, null);\n }\n });\n }\n\n function checkForOutdatedWrapper() {\n $http.get(\"internalapi/updates/isDisplayWrapperOutdated\").then(function (response) {\n var data = response.data;\n if (data !== undefined && data !== null && data) {\n ModalService.open(\"Outdated wrappers detected\", 'The NZBHydra wrappers (i.e. the executables or python scripts you use to run NZBHydra) seem to be outdated. Please update them.

          \\n' +\n ' Shut down NZBHydra, download the latest version and extract all the relevant wrapper files into your main NZBHydra folder.
          \\n' +\n ' For Windows these files are:\\n' +\n '
            \\n' +\n '
          • NZBHydra2.exe
          • \\n' +\n '
          • NZBHydra2 Console.exe
          • \\n' +\n '
          \\n' +\n ' For linux these files are:\\n' +\n '
            \\n' +\n '
          • nzbhydra2
          • \\n' +\n '
          • nzbhydra2wrapper.py
          • \\n' +\n '
          • nzbhydra2wrapperPy3.py
          • \\n' +\n '
          \\n' +\n ' Make sure to overwrite all of these files that already exist - you don\\'t need to update any files that aren\\'t already present.\\n' +\n '

          \\n' +\n ' Afterwards start NZBHydra again.', {\n yes: {\n text: \"OK\",\n onYes: function () {\n $http.put(\"internalapi/updates/setOutdatedWrapperDetectedWarningShown\")\n }\n }\n }, undefined, \"left\");\n\n }\n });\n }\n\n if ($scope.mayUpdate) {\n retrieveUpdateInfos();\n checkForOutOfMemoryException();\n checkForOutdatedWrapper();\n checkForOpenToInternet();\n checkForJavaBelow17();\n checkForFailedBackup();\n }\n\n function retrieveUpdateInfos() {\n $scope.checked = true;\n UpdateService.getInfos().then(function (response) {\n if (response) {\n $scope.currentVersion = response.data.currentVersion;\n $scope.latestVersion = response.data.latestVersion;\n $scope.latestVersionIsBeta = response.data.latestVersionIsBeta;\n $scope.updateAvailable = response.data.updateAvailable;\n $scope.changelog = response.data.changelog;\n $scope.updatedExternally = response.data.updatedExternally;\n $scope.showUpdateBannerOnUpdatedExternally = response.data.showUpdateBannerOnUpdatedExternally;\n $scope.showWhatsNewBanner = response.data.showWhatsNewBanner;\n if ($scope.updatedExternally && !$scope.showUpdateBannerOnUpdatedExternally) {\n $scope.updateAvailable = false;\n }\n $scope.automaticUpdateToNotice = response.data.automaticUpdateToNotice;\n\n\n $scope.$emit(\"showUpdateFooter\", $scope.updateAvailable);\n $scope.$emit(\"showAutomaticUpdateFooter\", $scope.automaticUpdateToNotice);\n } else {\n $scope.$emit(\"showUpdateFooter\", false);\n }\n });\n }\n\n $scope.update = function () {\n UpdateService.update($scope.latestVersion);\n };\n\n $scope.ignore = function () {\n UpdateService.ignore($scope.latestVersion);\n $scope.updateAvailable = false;\n $scope.$emit(\"showUpdateFooter\", $scope.updateAvailable);\n };\n\n $scope.showChangelog = function () {\n UpdateService.showChanges($scope.latestVersion);\n };\n\n $scope.showChangesFromAutomaticUpdate = function () {\n UpdateService.showChangesFromAutomaticUpdate();\n $scope.automaticUpdateToNotice = null;\n $scope.$emit(\"showAutomaticUpdateFooter\", false);\n };\n\n $scope.dismissChangesFromAutomaticUpdate = function () {\n $scope.automaticUpdateToNotice = null;\n $scope.$emit(\"showAutomaticUpdateFooter\", false);\n console.log(\"Dismissing showAutomaticUpdateFooter\");\n return $http.get(\"internalapi/updates/ackAutomaticUpdateVersionHistory\").then(function (response) {\n });\n };\n\n function checkAndShowNews() {\n RequestsErrorHandler.specificallyHandled(function () {\n if (ConfigService.getSafe().showNews) {\n $http.get(\"internalapi/news/forcurrentversion\").then(function (response) {\n var data = response.data;\n if (data && data.length > 0) {\n $uibModal.open({\n templateUrl: 'static/html/news-modal.html',\n controller: NewsModalInstanceCtrl,\n size: \"lg\",\n resolve: {\n news: function () {\n return data;\n }\n }\n });\n $http.put(\"internalapi/news/saveshown\");\n }\n });\n }\n });\n }\n\n function checkExpiredIndexers() {\n _.each(ConfigService.getSafe().indexers, function (indexer) {\n if (indexer.vipExpirationDate != null && indexer.vipExpirationDate !== \"Lifetime\") {\n var expiryWarning;\n var expiryDate = moment(indexer.vipExpirationDate, \"YYYY-MM-DD\");\n var messagePrefix = \"VIP access for indexer \" + indexer.name;\n if (expiryDate < moment()) {\n expiryWarning = messagePrefix + \" expired on \" + indexer.vipExpirationDate;\n } else if (expiryDate.subtract(7, 'days') < moment()) {\n expiryWarning = messagePrefix + \" will expire on \" + indexer.vipExpirationDate;\n }\n if (expiryWarning) {\n console.log(expiryWarning);\n growl.warning(expiryWarning);\n }\n }\n });\n }\n\n function checkAndShowWelcome() {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get(\"internalapi/welcomeshown\").then(function (response) {\n if (!response.data) {\n $http.put(\"internalapi/welcomeshown\");\n var promise = $uibModal.open({\n templateUrl: 'static/html/welcome-modal.html',\n controller: WelcomeModalInstanceCtrl,\n size: \"md\"\n });\n promise.opened.then(function () {\n welcomeIsBeingShown = true;\n });\n promise.closed.then(function () {\n welcomeIsBeingShown = false;\n });\n } else {\n if (HydraAuthService.getUserInfos().maySeeAdmin) {\n _.defer(checkAndShowNews);\n _.defer(checkExpiredIndexers);\n }\n }\n }, function () {\n console.log(\"Error while checking for welcome\")\n });\n });\n }\n\n checkAndShowWelcome();\n\n function showUnreadNotifications(unreadNotifications, stompClient) {\n if (unreadNotifications.length > ConfigService.getSafe().notificationConfig.displayNotificationsMax) {\n growl.info(unreadNotifications.length + ' notifications have piled up. Go to the notification history to view them.', {disableCountDown: true});\n for (var i = 0; i < unreadNotifications.length; i++) {\n if (unreadNotifications[i].id === undefined) {\n console.log(\"Undefined ID found for notification \" + unreadNotifications[i]);\n continue;\n }\n stompClient.send(\"/app/markNotificationRead\", {}, unreadNotifications[i].id);\n }\n return;\n }\n for (var j = 0; j < unreadNotifications.length; j++) {\n var notification = unreadNotifications[j];\n var body = notification.body.replace(\"\\n\", \"
          \");\n switch (notification.messageType) {\n case \"INFO\":\n growl.info(body);\n break;\n case \"SUCCESS\":\n growl.success(body);\n break;\n case \"WARNING\":\n growl.warning(body);\n break;\n case \"FAILURE\":\n growl.danger(body);\n break;\n }\n if (notification.id === undefined) {\n console.log(\"Undefined ID found for notification \" + unreadNotifications[i]);\n continue;\n }\n stompClient.send(\"/app/markNotificationRead\", {}, notification.id);\n }\n }\n\n if (ConfigService.getSafe().notificationConfig.displayNotifications && HydraAuthService.getUserInfos().maySeeAdmin) {\n var socket = new SockJS(bootstrapped.baseUrl + 'websocket');\n var stompClient = Stomp.over(socket);\n stompClient.debug = null;\n stompClient.connect({}, function (frame) {\n stompClient.subscribe('/topic/notifications', function (message) {\n showUnreadNotifications(JSON.parse(message.body), stompClient);\n });\n });\n }\n\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('NewsModalInstanceCtrl', NewsModalInstanceCtrl);\n\nfunction NewsModalInstanceCtrl($scope, $uibModalInstance, news) {\n $scope.news = news;\n $scope.close = function () {\n $uibModalInstance.dismiss();\n };\n}\n\nangular\n .module('nzbhydraApp')\n .controller('WelcomeModalInstanceCtrl', WelcomeModalInstanceCtrl);\n\nfunction WelcomeModalInstanceCtrl($scope, $uibModalInstance, $state, MigrationService) {\n $scope.close = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.startMigration = function () {\n $uibModalInstance.dismiss();\n MigrationService.migrate();\n };\n\n $scope.goToConfig = function () {\n $uibModalInstance.dismiss();\n $state.go(\"root.config.main\");\n }\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nangular\n .module('nzbhydraApp')\n .directive('footer', footer);\n\nfunction footer() {\n controller.$inject = [\"$scope\", \"$http\", \"$uibModal\", \"ConfigService\", \"GenericStorageService\", \"bootstrapped\"];\n return {\n templateUrl: 'static/html/directives/footer.html',\n controller: controller\n };\n\n function controller($scope, $http, $uibModal, ConfigService, GenericStorageService, bootstrapped) {\n $scope.updateFooterBottom = 0;\n\n var safeConfig = bootstrapped.safeConfig;\n $scope.showDownloaderStatus = safeConfig.downloading.showDownloaderStatus && _.filter(safeConfig.downloading.downloaders, function (x) {\n return x.enabled\n }).length > 0;\n $scope.showUpdateFooter = false;\n\n $scope.$on(\"showDownloaderStatus\", function (event, doShow) {\n $scope.showDownloaderStatus = doShow;\n updateFooterBottom();\n updatePaddingBottom();\n });\n $scope.$on(\"showUpdateFooter\", function (event, doShow) {\n $scope.showUpdateFooter = doShow;\n updateFooterBottom();\n updatePaddingBottom();\n });\n $scope.$on(\"showAutomaticUpdateFooter\", function (event, doShow) {\n $scope.showAutomaticUpdateFooter = doShow;\n updateFooterBottom();\n updatePaddingBottom();\n });\n\n function updateFooterBottom() {\n\n if ($scope.showDownloaderStatus) {\n if ($scope.showAutomaticUpdateFooter) {\n $scope.updateFooterBottom = 20;\n } else {\n $scope.updateFooterBottom = 38;\n }\n } else {\n $scope.updateFooterBottom = 0;\n }\n }\n\n function updatePaddingBottom() {\n var paddingBottom = 0;\n if ($scope.showDownloaderStatus) {\n paddingBottom += 30;\n }\n if ($scope.showUpdateFooter) {\n paddingBottom += 40;\n }\n $scope.paddingBottom = paddingBottom;\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-0\");\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-30\");\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-40\");\n document.getElementById(\"wrap\").classList.remove(\"padding-bottom-70\");\n var paddingBottomClass = \"padding-bottom-\" + paddingBottom;\n document.getElementById(\"wrap\").classList.add(paddingBottomClass);\n }\n\n updatePaddingBottom();\n\n updateFooterBottom();\n\n\n }\n}\n\n","angular\r\n .module('nzbhydraApp').directive('focusOn', focusOn);\r\n\r\nfunction focusOn() {\r\n return directive;\r\n\r\n function directive(scope, elem, attr) {\r\n scope.$on('focusOn', function (e, name) {\r\n if (name === attr.focusOn) {\r\n elem[0].focus();\r\n }\r\n });\r\n }\r\n}\r\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nangular\n .module('nzbhydraApp')\n .directive('downloaderStatusFooter', downloaderStatusFooter);\n\nfunction downloaderStatusFooter() {\n controller.$inject = [\"$scope\", \"$http\", \"RequestsErrorHandler\", \"HydraAuthService\", \"$interval\", \"bootstrapped\"];\n return {\n templateUrl: 'static/html/directives/downloader-status-footer.html',\n controller: controller\n };\n\n function controller($scope, $http, RequestsErrorHandler, HydraAuthService, $interval, bootstrapped) {\n\n var downloaderStatus;\n var updateInterval = null;\n console.log(\"websocket\");\n var socket = new SockJS(bootstrapped.baseUrl + 'websocket');\n var stompClient = Stomp.over(socket);\n stompClient.debug = null;\n stompClient.connect({}, function (frame) {\n stompClient.subscribe('/topic/downloaderStatus', function (message) {\n downloaderStatus = JSON.parse(message.body);\n updateFooter(downloaderStatus);\n });\n stompClient.send(\"/app/connectDownloaderStatus\", function (message) {\n downloaderStatus = JSON.parse(message.body);\n updateFooter(downloaderStatus);\n })\n });\n\n\n $scope.$emit(\"showDownloaderStatus\", true);\n var downloadRateCounter = 0;\n\n $scope.downloaderChart = {\n options: {\n chart: {\n type: 'stackedAreaChart',\n height: 35,\n width: 300,\n margin: {\n top: 5,\n right: 0,\n bottom: 0,\n left: 0\n },\n x: function (d) {\n return d.x;\n },\n y: function (d) {\n return d.y;\n },\n interactive: true,\n useInteractiveGuideline: false,\n transitionDuration: 0,\n showControls: false,\n showLegend: false,\n showValues: false,\n duration: 0,\n tooltip: {\n valueFormatter: function (d, i) {\n return d + \" kb/s\";\n },\n keyFormatter: function () {\n return \"\";\n },\n id: \"downloader-status-tooltip\"\n },\n css: \"float:right;\"\n }\n },\n data: [{values: [], key: \"Bla\", color: '#00a950'}],\n config: {\n refreshDataOnly: true,\n deepWatchDataDepth: 0,\n deepWatchData: false,\n deepWatchOptions: false\n }\n };\n\n function updateFooter() {\n if (downloaderStatus.lastUpdateForNow && updateInterval === null) {\n //Server will send no new status updates for a while because the last two retrieved statuses are the same.\n //We must still update the footer so that the graph doesn't stand still\n console.debug(\"Retrieved last update for now, starting update interval\");\n updateInterval = $interval(function () {\n //Just put the last known rate at the end to keep it going\n $scope.downloaderChart.data[0].values.splice(0, 1);\n $scope.downloaderChart.data[0].values.push({x: downloadRateCounter++, y: downloaderStatus.lastDownloadRate});\n try {\n $scope.api.update();\n } catch (ignored) {\n }\n if (_.every($scope.downloaderChart.data[0].values, function (value) {\n return value === downloaderStatus.lastDownloadRate\n })) {\n //The bar has been filled with the latest known value, we can now stop until we get a new update\n console.debug(\"Filled the bar with last known value, stopping update interval\");\n $interval.cancel(updateInterval);\n updateInterval = null;\n }\n }, 1000);\n } else if (updateInterval !== null && !downloaderStatus.lastUpdateForNow) {\n //New data is incoming, cancel interval\n console.debug(\"Got new update, stopping update interval\")\n $interval.cancel(updateInterval);\n updateInterval = null;\n }\n\n $scope.foo = downloaderStatus;\n $scope.foo.downloaderImage = downloaderStatus.downloaderType === 'NZBGET' ? 'nzbgetlogo' : 'sabnzbdlogo';\n $scope.foo.url = downloaderStatus.url;\n //We need to splice the variable with the rates because it's watched by angular and when overwriting it we would lose the watch and it wouldn't be updated\n var maxEntriesHistory = 200;\n if ($scope.downloaderChart.data[0].values.length < maxEntriesHistory) {\n //Not yet full, just fill up\n console.debug(\"Adding data, filling bar with initial values\")\n for (var i = $scope.downloaderChart.data[0].values.length; i < maxEntriesHistory; i++) {\n if (i >= downloaderStatus.downloadingRatesInKilobytes.length) {\n break;\n }\n $scope.downloaderChart.data[0].values.push({x: downloadRateCounter++, y: downloaderStatus.downloadingRatesInKilobytes[i]});\n }\n } else {\n console.debug(\"Adding data, moving bar\")\n //Remove first one, add to the end\n $scope.downloaderChart.data[0].values.splice(0, 1);\n $scope.downloaderChart.data[0].values.push({x: downloadRateCounter++, y: downloaderStatus.lastDownloadRate});\n }\n try {\n $scope.api.update();\n } catch (ignored) {\n }\n if ($scope.foo.state === \"DOWNLOADING\") {\n $scope.foo.buttonClass = \"play\";\n } else if ($scope.foo.state === \"PAUSED\") {\n $scope.foo.buttonClass = \"pause\";\n } else if ($scope.foo.state === \"OFFLINE\") {\n $scope.foo.buttonClass = \"off\";\n } else {\n $scope.foo.buttonClass = \"time\";\n }\n $scope.foo.state = $scope.foo.state.substr(0, 1) + $scope.foo.state.substr(1).toLowerCase();\n //Bad but without the state isn't updated\n $scope.$apply();\n }\n\n }\n}\n\n","angular\r\n .module('nzbhydraApp')\r\n .directive('downloadNzbzipButton', downloadNzbzipButton);\r\n\r\nfunction downloadNzbzipButton() {\r\n controller.$inject = [\"$scope\", \"growl\", \"$http\", \"FileDownloadService\"];\r\n return {\r\n templateUrl: 'static/html/directives/download-nzbzip-button.html',\r\n require: ['^searchResults'],\r\n scope: {\r\n searchResults: \"<\",\r\n searchTitle: \"<\",\r\n callback: \"&\"\r\n },\r\n controller: controller\r\n };\r\n\r\n\r\n function controller($scope, growl, $http, FileDownloadService) {\r\n $scope.download = function () {\r\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length === 0) {\r\n growl.info(\"You should select at least one result...\");\r\n } else {\r\n var values = _.map($scope.searchResults, function (value) {\r\n return value.searchResultId;\r\n });\r\n var link = \"internalapi/nzbzip\";\r\n\r\n var searchTitle;\r\n if (angular.isDefined($scope.searchTitle)) {\r\n searchTitle = \" for \" + $scope.searchTitle.replace(\"[^a-zA-Z0-9.-]\", \"_\");\r\n } else {\r\n searchTitle = \"\";\r\n }\r\n var filename = \"NZBHydra NZBs\" + searchTitle + \".zip\";\r\n $http({method: \"post\", url: link, data: values}).then(function (response) {\r\n if (response.data.successful && response.data.zip !== null) {\r\n link = \"internalapi/nzbzipDownload\";\r\n FileDownloadService.downloadFile(link, filename, \"POST\", response.data.zipFilepath);\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: response.data.addedIds});\r\n }\r\n if (response.data.missedIds.length > 0) {\r\n growl.error(\"Unable to add \" + response.missedIds.length + \" out of \" + values.length + \" NZBs to ZIP\");\r\n }\r\n } else {\r\n growl.error(response.data.message);\r\n }\r\n }, function (data, status, headers, config) {\r\n growl.error(status);\r\n });\r\n }\r\n }\r\n }\r\n}\r\n\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('downloadNzbsButton', downloadNzbsButton);\r\n\r\nfunction downloadNzbsButton() {\r\n controller.$inject = [\"$scope\", \"$http\", \"NzbDownloadService\", \"ConfigService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/download-nzbs-button.html',\r\n require: ['^searchResults'],\r\n scope: {\r\n searchResults: \"<\",\r\n callback: \"&\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, $http, NzbDownloadService, ConfigService, growl) {\r\n\r\n $scope.downloaders = NzbDownloadService.getEnabledDownloaders();\r\n $scope.blackholeEnabled = ConfigService.getSafe().downloading.saveTorrentsTo !== null;\r\n\r\n $scope.download = function (downloader) {\r\n if (angular.isUndefined($scope.searchResults) || $scope.searchResults.length === 0) {\r\n growl.info(\"You should select at least one result...\");\r\n } else {\r\n\r\n var didFilterOutResults = false;\r\n var didKeepAnyResults = false;\r\n var searchResults = _.filter($scope.searchResults, function (value) {\r\n if (value.downloadType === \"NZB\") {\r\n didKeepAnyResults = true;\r\n return true;\r\n } else {\r\n console.log(\"Not sending torrent result to downloader\");\r\n didFilterOutResults = true;\r\n return false;\r\n }\r\n });\r\n if (didFilterOutResults && !didKeepAnyResults) {\r\n growl.info(\"None of the selected results were NZBs. Adding aborted\");\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: []});\r\n }\r\n return;\r\n } else if (didFilterOutResults && didKeepAnyResults) {\r\n growl.info(\"Some the selected results are torrent results which were skipped\");\r\n }\r\n\r\n var tos = _.map(searchResults, function (entry) {\r\n return {searchResultId: entry.searchResultId, originalCategory: entry.originalCategory}\r\n });\r\n\r\n NzbDownloadService.download(downloader, tos).then(function (response) {\r\n if (angular.isDefined(response.data)) {\r\n if (response !== \"dismissed\") {\r\n if (response.data.successful) {\r\n if (response.data.message == null) {\r\n growl.info(\"Successfully added all NZBs\");\r\n } else {\r\n growl.warning(response.data.message);\r\n }\r\n } else {\r\n growl.error(response.data.message);\r\n }\r\n } else {\r\n growl.error(\"Error while adding NZBs\");\r\n }\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: response.data.addedIds});\r\n }\r\n }\r\n }, function () {\r\n growl.error(\"Error while adding NZBs\");\r\n });\r\n }\r\n };\r\n\r\n $scope.sendToBlackhole = function () {\r\n var didFilterOutResults = false;\r\n var didKeepAnyResults = false;\r\n var searchResults = _.filter($scope.searchResults, function (value) {\r\n if (value.downloadType === \"TORRENT\") {\r\n didKeepAnyResults = true;\r\n return true;\r\n } else {\r\n console.log(\"Not sending NZB result to black hole\");\r\n didFilterOutResults = true;\r\n return false;\r\n }\r\n });\r\n if (didFilterOutResults && !didKeepAnyResults) {\r\n growl.info(\"None of the selected results were torrents. Adding aborted\");\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: []});\r\n }\r\n return;\r\n } else if (didFilterOutResults && didKeepAnyResults) {\r\n growl.info(\"Some the selected results are NZB results which were skipped\");\r\n }\r\n var searchResultIds = _.pluck(searchResults, \"searchResultId\");\r\n $http.put(\"internalapi/saveTorrent\", searchResultIds).then(function (response) {\r\n if (response.data.successful) {\r\n growl.info(\"Successfully saved all torrents\");\r\n } else {\r\n growl.error(response.data.message);\r\n }\r\n if (angular.isDefined($scope.callback)) {\r\n $scope.callback({result: response.data.addedIds});\r\n }\r\n });\r\n }\r\n\r\n }\r\n}\r\n\r\n","\r\nfreetextFilter.$inject = [\"DebugService\"];\r\nbooleanFilter.$inject = [\"DebugService\"];angular\r\n .module('nzbhydraApp').directive(\"columnFilterWrapper\", columnFilterWrapper);\r\n\r\nfunction columnFilterWrapper() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: 'static/html/dataTable/columnFilterOuter.html',\r\n transclude: true,\r\n controllerAs: 'columnFilterWrapperCtrl',\r\n scope: {\r\n inline: \"@\"\r\n },\r\n bindToController: true,\r\n controller: controller,\r\n link: function (scope, element, attr, ctrl) {\r\n scope.element = element;\r\n }\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n var vm = this;\r\n\r\n vm.open = false;\r\n vm.isActive = false;\r\n\r\n vm.toggle = function () {\r\n vm.open = !vm.open;\r\n if (vm.open) {\r\n $scope.$broadcast(\"opened\");\r\n }\r\n };\r\n\r\n vm.clear = function () {\r\n if (vm.open) {\r\n $scope.$broadcast(\"clear\");\r\n }\r\n };\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive, open) {\r\n vm.open = open || false;\r\n vm.isActive = isActive;\r\n });\r\n\r\n DebugService.log(\"filter-wrapper\");\r\n }\r\n\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"freetextFilter\", freetextFilter);\r\n\r\nfunction freetextFilter(DebugService) {\r\n controller.$inject = [\"$scope\", \"focus\"];\r\n return {\r\n template: '',\r\n require: \"^columnFilterWrapper\",\r\n controllerAs: 'innerController',\r\n scope: {\r\n column: \"@\",\r\n onKey: \"@\",\r\n placeholder: \"@\",\r\n tooltip: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, focus) {\r\n $scope.inline = $scope.$parent.$parent.columnFilterWrapperCtrl.inline; //Hacky way of getting the value from the outer wrapper\r\n $scope.data = {};\r\n $scope.tooltip = $scope.tooltip || \"\";\r\n\r\n $scope.$on(\"opened\", function () {\r\n focus(\"freetext-filter-input\");\r\n });\r\n\r\n function emitFilterEvent(isOpen) {\r\n isOpen = $scope.inline || isOpen;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: $scope.data.filter,\r\n filterType: \"freetext\"\r\n }, angular.isDefined($scope.data.filter) && $scope.data.filter.length > 0, isOpen);\r\n }\r\n\r\n $scope.$on(\"clear\", function () {\r\n //Don't clear but close window (event is fired when clicked outside)\r\n emitFilterEvent(false);\r\n });\r\n\r\n $scope.onKeyUp = function (keyEvent) {\r\n if (keyEvent.which === 13 || $scope.onKey) {\r\n emitFilterEvent($scope.onKey && keyEvent.which !== 13); //Keep open if triggered by key, close always when enter pressed\r\n }\r\n };\r\n DebugService.log(\"filter-freetext\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"checkboxesFilter\", checkboxesFilter);\r\n\r\nfunction checkboxesFilter() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n template: '',\r\n controllerAs: 'checkboxesFilterController',\r\n scope: {\r\n column: \"@\",\r\n entries: \"<\",\r\n preselect: \"<\",\r\n showInvert: \"<\",\r\n isBoolean: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n $scope.selected = {\r\n entries: []\r\n };\r\n $scope.active = false;\r\n\r\n if ($scope.preselect) {\r\n $scope.selected.entries.push.apply($scope.selected.entries, $scope.entries);\r\n }\r\n\r\n $scope.invert = function () {\r\n $scope.selected.entries = _.difference($scope.entries, $scope.selected.entries);\r\n };\r\n\r\n $scope.selectAll = function () {\r\n $scope.selected.entries.push.apply($scope.selected.entries, $scope.entries);\r\n };\r\n\r\n $scope.deselectAll = function () {\r\n $scope.selected.entries.splice(0, $scope.selected.entries.length);\r\n };\r\n\r\n $scope.apply = function () {\r\n $scope.active = $scope.selected.entries.length < $scope.entries.length;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: _.pluck($scope.selected.entries, \"id\"),\r\n filterType: \"checkboxes\",\r\n isBoolean: $scope.isBoolean\r\n }, $scope.active)\r\n };\r\n $scope.clear = function () {\r\n $scope.selectAll();\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: undefined,\r\n filterType: \"checkboxes\",\r\n isBoolean: $scope.isBoolean\r\n }, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n DebugService.log(\"filter-checkboxes\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"booleanFilter\", booleanFilter);\r\n\r\nfunction booleanFilter(DebugService) {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n template: '',\r\n controllerAs: 'booleanFilterController',\r\n scope: {\r\n column: \"@\",\r\n options: \"<\",\r\n preselect: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n\r\n function controller($scope) {\r\n $scope.selected = {value: $scope.options[$scope.preselect].value};\r\n $scope.active = false;\r\n\r\n $scope.apply = function () {\r\n $scope.active = $scope.selected.value !== $scope.options[0].value;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: $scope.selected.value,\r\n filterType: \"boolean\"\r\n }, $scope.active)\r\n };\r\n $scope.clear = function () {\r\n $scope.selected.value = true;\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {filterValue: undefined, filterType: \"boolean\"}, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n DebugService.log(\"filter-boolean\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"timeFilter\", timeFilter);\r\n\r\nfunction timeFilter() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n template: '',\r\n scope: {\r\n column: \"@\",\r\n selected: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n $scope.active = false;\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n $scope.apply = function () {\r\n $scope.active = $scope.selected.beforeDate || $scope.selected.afterDate;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: {\r\n after: $scope.selected.afterDate,\r\n before: $scope.selected.beforeDate\r\n }, filterType: \"time\"\r\n }, $scope.active)\r\n };\r\n $scope.clear = function () {\r\n $scope.selected.beforeDate = undefined;\r\n $scope.selected.afterDate = undefined;\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {filterValue: undefined, filterType: \"time\"}, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n DebugService.log(\"filter-time\");\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"numberRangeFilter\", numberRangeFilter);\r\n\r\nfunction numberRangeFilter() {\r\n controller.$inject = [\"$scope\", \"DebugService\"];\r\n return {\r\n template: '',\r\n scope: {\r\n column: \"@\",\r\n min: \"<\",\r\n max: \"<\",\r\n addon: \"@\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, DebugService) {\r\n $scope.filterValue = {min: undefined, max: undefined};\r\n $scope.active = false;\r\n\r\n function apply() {\r\n $scope.active = $scope.filterValue.min || $scope.filterValue.max;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: $scope.filterValue,\r\n filterType: \"numberRange\"\r\n }, $scope.active)\r\n }\r\n\r\n $scope.clear = function () {\r\n $scope.filterValue = {min: undefined, max: undefined};\r\n $scope.active = false;\r\n $scope.$emit(\"filter\", $scope.column, {\r\n filterValue: undefined,\r\n filterType: \"numberRange\",\r\n isBoolean: $scope.isBoolean\r\n }, $scope.active)\r\n };\r\n $scope.$on(\"clear\", $scope.clear);\r\n\r\n $scope.apply = function () {\r\n apply();\r\n };\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n apply();\r\n }\r\n };\r\n\r\n DebugService.log(\"filter-number\");\r\n }\r\n}\r\n\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"columnSortable\", columnSortable);\r\n\r\nfunction columnSortable() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n restrict: \"E\",\r\n templateUrl: \"static/html/dataTable/columnSortable.html\",\r\n transclude: true,\r\n scope: {\r\n sortMode: \"<\", //0: no sorting, 1: asc, 2: desc\r\n column: \"@\",\r\n reversed: \"<\",\r\n startMode: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n if (angular.isUndefined($scope.sortMode)) {\r\n $scope.sortMode = 0;\r\n }\r\n\r\n if (angular.isUndefined($scope.startMode)) {\r\n $scope.startMode = 1;\r\n }\r\n\r\n $scope.sortModel = {\r\n sortMode: $scope.sortMode,\r\n column: $scope.column,\r\n reversed: $scope.reversed,\r\n startMode: $scope.startMode,\r\n active: false\r\n };\r\n\r\n $scope.$on(\"newSortColumn\", function (event, column, sortMode) {\r\n $scope.sortModel.active = column === $scope.sortModel.column;\r\n if (column !== $scope.sortModel.column) {\r\n $scope.sortModel.sortMode = 0;\r\n } else {\r\n $scope.sortModel.sortMode = sortMode;\r\n }\r\n });\r\n\r\n $scope.sort = function () {\r\n if ($scope.sortModel.sortMode === 0 || angular.isUndefined($scope.sortModel.sortMode)) {\r\n $scope.sortModel.sortMode = $scope.sortModel.startMode;\r\n } else if ($scope.sortModel.sortMode === 1) {\r\n $scope.sortModel.sortMode = 2;\r\n } else {\r\n $scope.sortModel.sortMode = 1;\r\n }\r\n $scope.$emit(\"sort\", $scope.sortModel.column, $scope.sortModel.sortMode, $scope.sortModel.reversed)\r\n };\r\n\r\n }\r\n}","angular\r\n .module('nzbhydraApp')\r\n .directive('connectionTest', connectionTest);\r\n\r\nfunction connectionTest() {\r\n controller.$inject = [\"$scope\"];\r\n return {\r\n templateUrl: 'static/html/directives/connection-test.html',\r\n require: ['^type', '^data'],\r\n scope: {\r\n type: \"=\",\r\n id: \"=\",\r\n data: \"=\",\r\n downloader: \"=\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope) {\r\n $scope.message = \"\";\r\n\r\n\r\n var testButton = \"#button-test-connection\";\r\n var testMessage = \"#message-test-connection\";\r\n\r\n function showSuccess() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-danger\");\r\n angular.element(testButton).addClass(\"btn-success\");\r\n }\r\n\r\n function showError() {\r\n angular.element(testButton).removeClass(\"btn-default\");\r\n angular.element(testButton).removeClass(\"btn-success\");\r\n angular.element(testButton).addClass(\"btn-danger\");\r\n }\r\n\r\n $scope.testConnection = function () {\r\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\r\n var myInjector = angular.injector([\"ng\"]);\r\n var $http = myInjector.get(\"$http\");\r\n var url;\r\n var params;\r\n if ($scope.type === \"downloader\") {\r\n url = \"internalapi/test_downloader\";\r\n params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};\r\n if ($scope.downloader === \"SABNZBD\") {\r\n params.apiKey = $scope.data.apiKey;\r\n params.url = $scope.data.url;\r\n } else {\r\n params.host = $scope.data.host;\r\n params.port = $scope.data.port;\r\n params.ssl = $scope.data.ssl;\r\n }\r\n } else if ($scope.data.type === \"newznab\") {\r\n url = \"internalapi/test_newznab\";\r\n params = {host: $scope.data.host, apiKey: $scope.data.apiKey};\r\n if (angular.isDefined($scope.data.username)) {\r\n params[\"username\"] = $scope.data.username;\r\n params[\"password\"] = $scope.data.password;\r\n }\r\n }\r\n $http.get(url, {params: params}).then(function (result) {\r\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click\r\n if (result.successful) {\r\n angular.element(testMessage).text(\"\");\r\n showSuccess();\r\n } else {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }\r\n\r\n }, function () {\r\n angular.element(testMessage).text(result.message);\r\n showError();\r\n }\r\n ).finally(function () {\r\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\r\n })\r\n }\r\n\r\n }\r\n}\r\n\r\n","//Taken from https://github.com/IamAdamJowett/angular-click-outside\r\n\r\nclickOutside.$inject = [\"$document\", \"$parse\", \"$timeout\"];\r\nfunction childOf(/*child node*/c, /*parent node*/p) { //returns boolean\r\n while ((c = c.parentNode) && c !== p) ;\r\n return !!c;\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive(\"clickOutside\", clickOutside);\r\n\r\n/**\r\n * @ngdoc directive\r\n * @name angular-click-outside.directive:clickOutside\r\n * @description Directive to add click outside capabilities to DOM elements\r\n * @requires $document\r\n * @requires $parse\r\n * @requires $timeout\r\n **/\r\nfunction clickOutside($document, $parse, $timeout) {\r\n return {\r\n restrict: 'A',\r\n link: function ($scope, elem, attr) {\r\n\r\n // postpone linking to next digest to allow for unique id generation\r\n $timeout(function () {\r\n var classList = (attr.outsideIfNot !== undefined) ? attr.outsideIfNot.split(/[ ,]+/) : [],\r\n fn;\r\n\r\n function eventHandler(e) {\r\n var i,\r\n element,\r\n r,\r\n id,\r\n classNames,\r\n l;\r\n\r\n // check if our element already hidden and abort if so\r\n if (angular.element(elem).hasClass(\"ng-hide\")) {\r\n return;\r\n }\r\n\r\n // if there is no click target, no point going on\r\n if (!e || !e.target) {\r\n return;\r\n }\r\n\r\n if (angular.isDefined(attr.outsideIgnore) && $scope.$eval(attr.outsideIgnore)) {\r\n return;\r\n }\r\n var isChild = childOf(e.target, elem.context);\r\n if (isChild) {\r\n return;\r\n }\r\n // loop through the available elements, looking for classes in the class list that might match and so will eat\r\n for (element = e.target; element; element = element.parentNode) {\r\n // check if the element is the same element the directive is attached to and exit if so (props @CosticaPuntaru)\r\n if (element === elem[0]) {\r\n return;\r\n }\r\n\r\n // now we have done the initial checks, start gathering id's and classes\r\n id = element.id,\r\n classNames = element.className,\r\n l = classList.length;\r\n\r\n // Unwrap SVGAnimatedString classes\r\n if (classNames && classNames.baseVal !== undefined) {\r\n classNames = classNames.baseVal;\r\n }\r\n\r\n // if there are no class names on the element clicked, skip the check\r\n if (classNames || id) {\r\n\r\n // loop through the elements id's and classnames looking for exceptions\r\n for (i = 0; i < l; i++) {\r\n //prepare regex for class word matching\r\n r = new RegExp('\\\\b' + classList[i] + '\\\\b');\r\n\r\n // check for exact matches on id's or classes, but only if they exist in the first place\r\n if ((id !== undefined && id === classList[i]) || (classNames && r.test(classNames))) {\r\n // now let's exit out as it is an element that has been defined as being ignored for clicking outside\r\n return;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // if we have got this far, then we are good to go with processing the command passed in via the click-outside attribute\r\n $timeout(function () {\r\n fn = $parse(attr['clickOutside']);\r\n fn($scope, {event: e});\r\n });\r\n }\r\n\r\n // if the devices has a touchscreen, listen for this event\r\n if (_hasTouch()) {\r\n $document.on('touchstart', eventHandler);\r\n }\r\n\r\n // still listen for the click event even if there is touch to cater for touchscreen laptops\r\n $document.on('click', eventHandler);\r\n\r\n // when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around\r\n $scope.$on('$destroy', function () {\r\n if (_hasTouch()) {\r\n $document.off('touchstart', eventHandler);\r\n }\r\n\r\n $document.off('click', eventHandler);\r\n });\r\n\r\n /**\r\n * @description Private function to attempt to figure out if we are on a touch device\r\n * @private\r\n **/\r\n function _hasTouch() {\r\n // works on most browsers, IE10/11 and Surface\r\n return 'ontouchstart' in window || navigator.maxTouchPoints;\r\n }\r\n });\r\n }\r\n };\r\n}\r\n","angular\r\n .module('nzbhydraApp')\r\n .directive('cfgFormEntry', cfgFormEntry);\r\n\r\nfunction cfgFormEntry() {\r\n return {\r\n templateUrl: 'static/html/directives/cfg-form-entry.html',\r\n require: [\"^title\", \"^cfg\"],\r\n scope: {\r\n title: \"@\",\r\n cfg: \"=\",\r\n help: \"@\",\r\n type: \"@?\",\r\n options: \"=?\"\r\n },\r\n controller: [\"$scope\", \"$element\", \"$attrs\", function ($scope, $element, $attrs) {\r\n $scope.type = angular.isDefined($scope.type) ? $scope.type : 'text';\r\n $scope.options = angular.isDefined($scope.type) ? $scope.$eval($attrs.options) : [];\r\n }]\r\n };\r\n}","angular\n .module('nzbhydraApp')\n .directive('hydrabackup', hydrabackup);\n\nfunction hydrabackup() {\n controller.$inject = [\"$scope\", \"BackupService\", \"Upload\", \"FileDownloadService\", \"$http\", \"RequestsErrorHandler\", \"growl\", \"RestartService\"];\n return {\n templateUrl: 'static/html/directives/backup.html',\n controller: controller\n };\n\n function controller($scope, BackupService, Upload, FileDownloadService, $http, RequestsErrorHandler, growl, RestartService) {\n $scope.refreshBackupList = function () {\n BackupService.getBackupsList().then(function (backups) {\n $scope.backups = backups;\n });\n };\n\n $scope.refreshBackupList();\n\n $scope.uploadActive = false;\n\n\n $scope.createBackupFile = function () {\n $http.get(\"internalapi/backup/backuponly\", {params: {dontdownload: true}}).then(function () {\n $scope.refreshBackupList();\n });\n };\n $scope.createAndDownloadBackupFile = function () {\n FileDownloadService.downloadFile(\"internalapi/backup/backup\", \"nzbhydra-backup-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\", \"GET\").then(function () {\n $scope.refreshBackupList();\n });\n };\n\n $scope.uploadBackupFile = function (file, errFiles) {\n RequestsErrorHandler.specificallyHandled(function () {\n\n $scope.file = file;\n $scope.errFile = errFiles && errFiles[0];\n if (file) {\n $scope.uploadActive = true;\n file.upload = Upload.upload({\n url: 'internalapi/backup/restorefile',\n file: file\n });\n\n file.upload.then(function (response) {\n if (response.data.successful) {\n $scope.uploadActive = false;\n RestartService.startCountdown(\"Upload successful. Restarting for wrapper to restore data.\");\n } else {\n file.progress = 0;\n growl.error(response.data.message)\n }\n\n }, function (response) {\n growl.error(response.data.message)\n }, function (evt) {\n file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));\n file.loaded = Math.floor(evt.loaded / 1024);\n file.total = Math.floor(evt.total / 1024);\n });\n }\n });\n };\n\n $scope.restoreFromFile = function (filename) {\n BackupService.restoreFromFile(filename).then(function () {\n RestartService.startCountdown(\"Extraction of backup successful. Restarting for wrapper to restore data.\");\n },\n function (response) {\n growl.error(response.data);\n })\n }\n\n }\n}\n\n","\naddableNzbs.$inject = [\"DebugService\"];angular\n .module('nzbhydraApp')\n .directive('addableNzbs', addableNzbs);\n\nfunction addableNzbs(DebugService) {\n controller.$inject = [\"$scope\", \"NzbDownloadService\"];\n return {\n templateUrl: 'static/html/directives/addable-nzbs.html',\n require: [],\n scope: {\n searchresult: \"<\",\n alwaysAsk: \"<\"\n },\n controller: controller\n };\n\n function controller($scope, NzbDownloadService) {\n $scope.alwaysAsk = $scope.alwaysAsk === \"true\";\n $scope.downloaders = _.filter(NzbDownloadService.getEnabledDownloaders(), function (downloader) {\n if ($scope.searchresult.downloadType !== \"NZB\") {\n return downloader.downloadType === $scope.searchresult.downloadType\n }\n return true;\n });\n }\n}\n","\r\naddableNzb.$inject = [\"DebugService\"];angular\r\n .module('nzbhydraApp')\r\n .directive('addableNzb', addableNzb);\r\n\r\nfunction addableNzb(DebugService) {\r\n controller.$inject = [\"$scope\", \"NzbDownloadService\", \"growl\"];\r\n return {\r\n templateUrl: 'static/html/directives/addable-nzb.html',\r\n scope: {\r\n searchresult: \"=\",\r\n downloader: \"<\",\r\n alwaysAsk: \"<\"\r\n },\r\n controller: controller\r\n };\r\n\r\n function controller($scope, NzbDownloadService, growl) {\r\n if (!_.isNullOrEmpty($scope.downloader.iconCssClass)) {\r\n $scope.cssClass = \"fa fa-\" + $scope.downloader.iconCssClass.replace(\"fa-\", \"\").replace(\"fa \", \"\");\r\n } else {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd\" : \"nzbget\";\r\n }\r\n\r\n $scope.add = function () {\r\n var originalClass = $scope.cssClass;\r\n $scope.cssClass = \"nzb-spinning\";\r\n NzbDownloadService.download($scope.downloader, [{\r\n searchResultId: $scope.searchresult.searchResultId ? $scope.searchresult.searchResultId : $scope.searchresult.id,\r\n originalCategory: $scope.searchresult.originalCategory,\r\n mappedCategory: $scope.searchresult.category\r\n }], $scope.alwaysAsk).then(function (response) {\r\n if (response !== \"dismissed\") {\r\n if (response.data.successful && (response.data.addedIds != null && response.data.addedIds.indexOf(Number($scope.searchresult.searchResultId)) > -1)) {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd-success\" : \"nzbget-success\";\r\n } else {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(response.data.message);\r\n }\r\n } else {\r\n $scope.cssClass = originalClass;\r\n }\r\n }, function () {\r\n $scope.cssClass = $scope.downloader.downloaderType === \"SABNZBD\" ? \"sabnzbd-error\" : \"nzbget-error\";\r\n growl.error(\"An unexpected error occurred while trying to contact NZBHydra or add the NZB.\");\r\n })\r\n };\r\n }\r\n}","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nCheckCapsModalInstanceCtrl.$inject = [\"$scope\", \"$interval\", \"$http\", \"$timeout\", \"growl\", \"capsCheckRequest\"];\nIndexerConfigBoxService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\nIndexerCheckBeforeCloseService.$inject = [\"$q\", \"ModalService\", \"IndexerConfigBoxService\", \"growl\", \"blockUI\"];\nfunction regexValidator(regex, message, prefixViewValue, preventEmpty) {\n return {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n if (Array.isArray(value)) {\n for (var i = 0; i < value.length; i++) {\n if (!regex.test(value[i])) {\n return false;\n }\n }\n return true;\n } else {\n return regex.test(value);\n }\n }\n return !preventEmpty;\n },\n message: (prefixViewValue ? '$viewValue + \" ' : '\" ') + message + '\"'\n };\n}\n\nfunction getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesService) {\n var fieldset = [];\n if (indexerModel.searchModuleType === \"TORZNAB\") {\n fieldset.push({\n type: 'help',\n templateOptions: {\n type: 'help',\n lines: [\"Torznab indexers can only be used for internal searches or dedicated searches using /torznab/api\"]\n }\n });\n }\n if ((indexerModel.searchModuleType === \"NEWZNAB\" || indexerModel.searchModuleType === \"TORZNAB\") && !isInitial && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n var message;\n var cssClass;\n if (!indexerModel.configComplete) {\n message = \"The config of this indexer is incomplete. Please click the button at the bottom to check its capabilities and complete its configuration.\";\n cssClass = \"alert alert-danger\";\n } else {\n message = \"The capabilities of this indexer were not checked completely. Some actually supported search types or IDs may not be usable.\";\n cssClass = \"alert alert-warning\";\n }\n fieldset.push({\n type: 'help',\n hideExpression: 'model.allCapsChecked && model.configComplete',\n templateOptions: {\n type: 'help',\n lines: [message],\n class: cssClass\n }\n });\n }\n\n var stateHelp = \"\";\n if (indexerModel.state === \"DISABLED_SYSTEM_TEMPORARY\" || indexerModel.state === \"DISABLED_SYSTEM\") {\n if (indexerModel.state === \"DISABLED_SYSTEM_TEMPORARY\") {\n stateHelp = \"The indexer was disabled by the program due to an error. It will be reenabled automatically or you can enable it manually\";\n } else {\n stateHelp = \"The indexer was disabled by the program due to error from which it cannot recover by itself. Try checking the caps to make sure it works or just enable it and see what happens.\";\n }\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push(\n {\n key: 'name',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Name',\n required: true\n },\n validators: {\n uniqueName: {\n expression: function (viewValue) {\n if (isInitial || viewValue !== indexerModel.name) {\n return _.pluck(parentModel, \"name\").indexOf(viewValue) === -1;\n }\n return true;\n },\n message: '\"Indexer \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\n },\n noComma:\n {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n return value.indexOf(\",\") === -1;\n }\n return true;\n },\n message: '\"Name may not contain a comma\"'\n }\n }\n })\n }\n\n if (indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n fieldset.push({\n key: 'state',\n type: 'horizontalIndexerStateSwitch',\n templateOptions: {\n type: 'switch',\n label: 'State',\n help: stateHelp\n }\n });\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') {\n var hostField = {\n key: 'host',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Host',\n required: true,\n placeholder: 'http://www.someindexer.com'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n };\n if (indexerModel.searchModuleType === 'TORZNAB') {\n hostField.templateOptions.help = 'If you use Jackett and have an external URL use that one';\n }\n fieldset.push(\n hostField\n );\n }\n\n if ((indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') && indexerModel.host !== 'https://feed.animetosho.org') {\n fieldset.push(\n {\n key: 'apiKey',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'API Key'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n )\n }\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push(\n {\n key: 'apiPath',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'API path',\n help: 'Path to the API. If empty /api is used',\n required: false,\n advanced: true\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') {\n fieldset.push(\n {\n key: 'username',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'Username',\n help: 'Only needed if indexer requires HTTP auth for API access (rare).'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n );\n fieldset.push(\n {\n key: 'password',\n type: 'passwordSwitch',\n hideExpression: '!model.username',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'Password',\n help: 'Only needed if indexer requires HTTP auth for API access (rare).'\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n fieldset.push(\n {\n key: 'score',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Priority',\n required: true,\n help: 'When duplicate search results are found the result from the indexer with the highest number will be selected.',\n tooltip: 'The priority determines which indexer is used if duplicate results are found (i.e. results that link to the same upload, not just results with the same name).
          The result from the indexer with the highest number is shown first in the GUI and returned for API searches.'\n\n }\n });\n }\n\n fieldset.push(\n {\n key: 'timeout',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Timeout',\n min: 1,\n help: 'Supercedes the general timeout in \"Searching\".',\n advanced: true\n }\n },\n {\n key: 'schedule',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Schedule',\n help: 'Determines when an indexer should be selected. See wiki. You can enter multiple time spans. Apply values with return key.',\n advanced: true\n }\n }\n );\n\n if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push(\n {\n key: 'hitLimit',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'API hit limit',\n help: 'Maximum number of API hits since \"API hit reset time\".',\n tooltip: 'When the maximum number of API hits is reached the indexer isn\\'t used anymore. Only API hits done by NZBHydra are taken into account.'\n },\n validators: {\n greaterThanZero: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return _.isNullOrEmpty(value) || value > 0;\n },\n message: '\"Value must be greater than 0\"'\n }\n }\n },\n {\n key: 'downloadLimit',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Download limit',\n help: 'When # of downloads since \"Hit reset time\" is reached indexer will not be searched.'\n },\n validators: {\n greaterThanZero: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return _.isNullOrEmpty(value) || value > 0;\n },\n message: '\"Value must be greater than 0\"'\n }\n }\n }\n );\n fieldset.push(\n {\n key: 'hitLimitResetTime',\n type: 'horizontalInput',\n hideExpression: '!model.hitLimit && !model.downloadLimit',\n templateOptions: {\n type: 'number',\n label: 'Hit reset time',\n help: 'UTC hour of day at which the API hit counter is reset (0-23). Leave empty for a rolling reset counter.',\n tooltip: 'Either define the time of day when the counter is reset by the indexer or leave it empty to use a rolling reset counter, meaning the number of hits for the last 24h at the time of the search is limited.'\n },\n validators: {\n timeOfDay: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return value >= 0 && value <= 23;\n },\n message: '$viewValue + \" is not a valid hour of day (0-23)\"'\n }\n }\n },\n {\n key: 'loadLimitOnRandom',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Load limiting',\n help: 'If set indexer will only be picked for one out of x API searches (on average).',\n tooltip: 'For indexers with a low API hit limit you can enable load limiting. Define any number n so that the indexer will only be used for searches in 1/n cases (on average). For example if you define a load limit of 5 the indexer will only be picked every fifth search.',\n advanced: true\n },\n validators: {\n greaterThanZero: {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n return _.isNullOrEmpty(value) || value > 1;\n },\n message: '\"Value must be greater than 1\"'\n }\n }\n }\n );\n }\n if (indexerModel.searchModuleType === 'TORZNAB') {\n fieldset.push({\n key: 'minSeeders',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Minimum # seeders',\n help: 'Torznab results with fewer seeders will be ignored. Supercedes any setting made in the searching config.'\n }\n })\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB') {\n fieldset.push(\n {\n key: 'userAgent',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'User agent',\n help: 'Rarely needed. Will supercede the one in the main searching settings.',\n advanced: true\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType === 'NEWZNAB') {\n fieldset.push(\n {\n key: 'customParameters',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n required: false,\n label: 'Custom parameters',\n help: 'Define custom parameters to be sent to the indexer when searching. Use the format \"name=value\"Apply values with return key.',\n advanced: 'true'\n }\n }\n )\n }\n\n\n fieldset.push(\n {\n key: 'preselect',\n type: 'horizontalSwitch',\n hideExpression: 'model.enabledForSearchSource===\"EXTERNAL\"',\n templateOptions: {\n type: 'switch',\n label: 'Preselect',\n help: 'Preselect this indexer on the search page.'\n }\n }\n );\n fieldset.push(\n {\n key: 'enabledForSearchSource',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Enable for...',\n options: [\n {name: 'Internal searches only', value: 'INTERNAL'},\n {name: 'API searches only', value: 'API'},\n {name: 'All but API update queries ', value: 'ALL_BUT_RSS'},\n {name: 'Only API update queries ', value: 'ONLY_RSS'},\n {name: 'Internal and any API searches', value: 'BOTH'}\n ],\n help: 'Select for which searches this indexer will be used. \"Update queries\" are searches without query or ID (e.g. done by Sonarr periodically).',\n advanced: true\n }\n }\n );\n\n fieldset.push(\n {\n key: 'color',\n type: 'colorInput',\n templateOptions: {\n label: 'Color',\n help: 'If set it will be used in the search results to mark the indexer\\'s results.',\n tooltip: 'To mark expanded results they\\'re shown in a darker shade so it\\'s recommended to use indexer colors which not only differ in lightness',\n advanced: true\n }\n }\n );\n\n fieldset.push(\n {\n key: 'vipExpirationDate',\n type: 'horizontalInput',\n templateOptions: {\n required: false,\n label: 'VIP expiry',\n help: 'Enter when your VIP access expires and NZBHydra will track it and warn you when close to expiry. Enter as YYYY-MM-DD or \"Lifetime\".'\n },\n validators: {\n port: regexValidator(/^(\\d{4}-\\d{2}-\\d{2})|Lifetime$/, \"is no valid date (must be 'YYYY-MM-DD' or 'Lifetime')\", true, false)\n }\n }\n );\n\n if (indexerModel.searchModuleType !== \"ANIZB\" && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n var cats = CategoriesService.getWithoutAll();\n var options = _.map(cats, function (x) {\n return {id: x.name, label: x.name}\n });\n fieldset.push(\n {\n key: 'enabledCategories',\n type: 'horizontalMultiselect',\n templateOptions: {\n label: 'Categories',\n help: 'Only use indexer when searching for these and also reject results from others. Selecting none equals selecting all.',\n options: options,\n settings: {\n showSelectedValues: false,\n noSelectedText: \"None/All\"\n },\n advanced: true\n }\n }\n );\n }\n\n\n if ((indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') && !isInitial && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {\n fieldset.push(\n {\n key: 'supportedSearchIds',\n type: 'horizontalMultiselect',\n templateOptions: {\n label: 'Search IDs',\n options: [\n {label: 'IMDB (TV)', id: 'TVIMDB'},\n {label: 'TVDB', id: 'TVDB'},\n {label: 'TVRage', id: 'TVRAGE'},\n {label: 'Trakt', id: 'TRAKT'},\n {label: 'TVMaze', id: 'TVMAZE'},\n {label: 'IMDB', id: 'IMDB'},\n {label: 'TMDB', id: 'TMDB'}\n ],\n noSelectedText: \"None\",\n advanced: true\n }\n }\n );\n fieldset.push(\n {\n key: 'supportedSearchTypes',\n type: 'horizontalMultiselect',\n templateOptions: {\n label: 'Search types',\n options: [\n {label: 'Audio', id: 'AUDIO'},\n {label: 'Ebooks', id: 'BOOK'},\n {label: 'Movies', id: 'MOVIE'},\n {label: 'Search', id: 'SEARCH'},\n {label: 'TV', id: 'TVSEARCH'}\n ],\n buttonText: \"None\",\n advanced: true\n }\n }\n );\n fieldset.push(\n {\n type: 'horizontalCheckCaps',\n hideExpression: '!model.host || !model.name',\n templateOptions: {\n label: 'Check capabilities',\n help: 'Find out what search types and IDs the indexer supports.',\n tooltip: 'The first time an indexer is added the connection is tested. When successful the supported search IDs and types are checked. These determine if indexers allow searching for movies, shows or ebooks using meta data like the IMDB id or the author and title. Newznab indexers cannot be used until this check was completed. Click this button to execute the caps check again.'\n }\n }\n )\n }\n\n if (indexerModel.searchModuleType === 'nzbindex') {\n fieldset.push(\n {\n key: 'generalMinSize',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Min size',\n help: 'NZBIndex returns a lot of crap with small file sizes. Set this value and all smaller results will be filtered out no matter the category'\n }\n }\n );\n }\n\n return fieldset;\n}\n\n\nfunction _showBox(indexerModel, parentModel, isInitial, $uibModal, CategoriesService, mode, form, callback) {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/config/indexer-config-box.html',\n controller: 'IndexerConfigBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n indexerModel.showAdvanced = parentModel.showAdvanced;\n return indexerModel;\n },\n fields: function () {\n return getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesService, mode);\n },\n form: function () {\n return form;\n },\n isInitial: function () {\n return isInitial\n },\n parentModel: function () {\n return parentModel;\n }\n }\n });\n\n\n modalInstance.result.then(function (returnedModel) {\n form.$setDirty(true);\n if (angular.isDefined(callback)) {\n callback(true, returnedModel);\n }\n }, function () {\n if (angular.isDefined(callback)) {\n callback(false);\n }\n });\n}\n\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n\n formlyConfigProvider.setType({\n name: 'indexers',\n templateUrl: 'static/html/config/indexer-config.html',\n controller: function ($scope, $uibModal, growl, CategoriesService) {\n $scope.showBox = showBox;\n $scope.formOptions = {formState: $scope.formState};\n $scope.showPresetSelection = showPresetSelection;\n\n function showPresetSelection() {\n $uibModal.open({\n templateUrl: 'static/html/config/indexer-config-selection.html',\n controller: 'IndexerConfigSelectionBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n return $scope.model;\n },\n form: function () {\n return $scope.form;\n }\n }\n });\n }\n\n //Called when clicking the box of an existing indexer\n function showBox(indexerModel, model) {\n _showBox(indexerModel, model, false, $uibModal, CategoriesService, \"indexer\", $scope.form)\n }\n\n }\n });\n }]);\n\n\nangular.module('nzbhydraApp').controller('IndexerConfigSelectionBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$uibModal\", \"$http\", \"model\", \"form\", \"growl\", \"CategoriesService\", \"$timeout\", function ($scope, $q, $uibModalInstance, $uibModal, $http, model, form, growl, CategoriesService, $timeout) {\n\n $scope.showBox = showBox;\n $scope.isInitial = false;\n\n $scope.select = function (modelPreset) {\n\n addEntry(modelPreset);\n $timeout(function () {\n $uibModalInstance.close();\n },\n 200);\n };\n\n $scope.readJackettConfig = function () {\n\n var indexerModel = createIndexerModel();\n indexerModel.searchModuleType = \"JACKETT_CONFIG\";\n indexerModel.isInitial = false;\n indexerModel.host = \"http://127.0.0.1:9117\";\n indexerModel.name = \"Jackett config\";\n _showBox(indexerModel, model, true, $uibModal, CategoriesService, \"jackettConfig\", form, function (isSubmitted, returnedModel) {\n if (isSubmitted) {\n //User pushed button, now we read the config\n $http.post(\"internalapi/indexer/readJackettConfig\", {existingIndexers: model, jackettConfig: returnedModel}, {\n headers: {\n \"Accept\": \"application/json;charset=utf-8\",\n \"Accept-Charset\": \"charset=utf-8\"\n }\n }).then(function (response) {\n //Replace model with new result\n model.splice(0, model.length);\n _.each(response.data.newIndexersConfig, function (x) {\n model.push(x);\n });\n growl.info(\"Added \" + response.data.addedTrackers + \" new trackers from Jackett\");\n growl.info(\"Updated \" + response.data.updatedTrackers + \" trackers from Jackett\");\n\n }, function (response) {\n ModalService.open(\"Error reading jackett config\", response.data, {}, \"md\", \"left\");\n });\n }\n });\n\n $timeout(function () {\n $uibModalInstance.close();\n },\n 200);\n };\n\n function showBox(indexerModel, model) {\n _showBox(indexerModel, model, false, $uibModal, CategoriesService, \"indexer\", form)\n }\n\n function createIndexerModel() {\n return angular.copy({\n allCapsChecked: false,\n apiKey: null,\n backend: 'NEWZNAB',\n color: null,\n configComplete: false,\n categoryMapping: null,\n downloadLimit: null,\n enabledCategories: [],\n enabledForSearchSource: \"BOTH\",\n generalMinSize: null,\n hitLimit: null,\n hitLimitResetTime: 0,\n host: null,\n loadLimitOnRandom: null,\n name: null,\n password: null,\n preselect: true,\n score: 0,\n searchModuleType: 'NEWZNAB',\n showOnSearch: true,\n state: \"ENABLED\",\n supportedSearchIds: undefined,\n supportedSearchTypes: undefined,\n timeout: null,\n username: null,\n userAgent: null\n });\n }\n\n function addEntry(preset) {\n if (checkAddingAllowed(model, preset)) {\n var indexerModel = createIndexerModel();\n if (angular.isDefined(preset)) {\n _.extend(indexerModel, preset);\n }\n\n $scope.isInitial = true;\n\n _showBox(indexerModel, model, true, $uibModal, CategoriesService, \"indexer\", form, function (isSubmitted, returnedModel) {\n if (isSubmitted) {\n //Here is where the entry is actually added to the model\n model.push(angular.isDefined(returnedModel) ? returnedModel : indexerModel);\n }\n });\n } else {\n growl.error(\"That predefined indexer is already configured.\"); //For now this is the only case where adding is forbidden so we use this hardcoded message \"for now\"... (;-))\n }\n }\n\n function checkAddingAllowed(existingIndexers, preset) {\n if (!preset || !(preset.searchModuleType === \"ANIZB\" || preset.searchModuleType === \"BINSEARCH\" || preset.searchModuleType === \"NZBINDEX\" || preset.searchModuleType === \"NZBCLUB\")) {\n return true;\n }\n return !_.any(existingIndexers, function (existingEntry) {\n return existingEntry.name === preset.name;\n });\n }\n\n $scope.newznabPresets = [\n {\n name: \"abNZB\",\n host: \"https://abnzb.com/\"\n },\n {\n name: \"altHUB\",\n host: \"https://api.althub.co.za\"\n },\n {\n name: \"Animetosho (Newznab)\",\n host: \"https://feed.animetosho.org\",\n categories: [\"Anime\"],\n supportedSearchIds: [],\n supportedSearchTypes: [\"SEARCH\"],\n allCapsChecked: true,\n configComplete: true\n },\n {\n name: \"DogNZB\",\n host: \"https://api.dognzb.cr\"\n },\n {\n name: \"Drunken Slug\",\n host: \"https://api.drunkenslug.com\"\n },\n {\n name: \"FastNZB\",\n host: \"https://fastnzb.com\"\n },\n {\n name: \"LuluNZB\",\n host: \"https://lulunzb.com\"\n },\n {\n name: \"miatrix\",\n host: \"https://www.miatrix.com\"\n },\n {\n name: \"NZB Finder\",\n host: \"https://nzbfinder.ws\"\n },\n {\n name: \"NZBCat\",\n host: \"https://nzb.cat\"\n },\n {\n name: \"nzb.su\",\n host: \"https://api.nzb.su\"\n },\n {\n name: \"NZBGeek\",\n host: \"https://api.nzbgeek.info\"\n },\n {\n name: \"NzbNdx\",\n host: \"https://www.nzbndx.com\"\n },\n {\n name: \"NzBNooB\",\n host: \"https://www.nzbnoob.com\"\n },\n {\n name: \"NzbNation\",\n host: \"http://www.nzbnation.com/\"\n },\n {\n name: \"nzbplanet\",\n host: \"https://nzbplanet.net\"\n },\n {\n name: \"omgwtfnzbs\",\n host: \"https://api.omgwtfnzbs.me\"\n },\n {\n name: \"spotweb.com\",\n host: \"https://spotweb.me\"\n },\n {\n name: \"Tabula-Rasa\",\n host: \"https://www.tabula-rasa.pw/api/v1/\"\n },\n {\n allCapsChecked: true,\n enabledForSearchSource: \"INTERNAL\",\n categories: [],\n configComplete: true,\n downloadLimit: null,\n hitLimit: null,\n hitLimitResetTime: null,\n host: \"https://binsearch.info\",\n loadLimitOnRandom: null,\n name: \"Binsearch\",\n password: null,\n preselect: true,\n score: 0,\n showOnSearch: true,\n state: \"ENABLED\",\n supportedSearchIds: [],\n supportedSearchTypes: [],\n timeout: null,\n searchModuleType: \"BINSEARCH\",\n username: null\n },\n {\n allCapsChecked: true,\n enabledForSearchSource: \"INTERNAL\",\n categories: [],\n configComplete: true,\n downloadLimit: null,\n generalMinSize: 1,\n hitLimit: null,\n hitLimitResetTime: null,\n host: \"https://nzbindex.com\",\n loadLimitOnRandom: null,\n name: \"NZBIndex\",\n password: null,\n preselect: true,\n score: 0,\n showOnSearch: true,\n state: \"ENABLED\",\n supportedSearchIds: [],\n supportedSearchTypes: [],\n timeout: null,\n searchModuleType: \"NZBINDEX\",\n username: null\n }\n ];\n $scope.newznabPresets = _.sortBy($scope.newznabPresets, function (entry) {\n return entry.name.toLowerCase()\n });\n\n $scope.torznabPresets = [\n {\n allCapsChecked: false,\n configComplete: false,\n name: \"Jackett/Cardigann\",\n host: \"http://127.0.0.1:9117/api/v2.0/indexers/YOURTRACKER/results/torznab/\",\n supportedSearchIds: undefined,\n supportedSearchTypes: undefined,\n searchModuleType: \"TORZNAB\",\n state: \"ENABLED\",\n enabledForSearchSource: \"BOTH\"\n },\n {\n categories: [\"Anime\"],\n allCapsChecked: true,\n configComplete: true,\n name: \"Animetosho (Torznab)\",\n host: \"https://feed.animetosho.org\",\n supportedSearchIds: [],\n supportedSearchTypes: [\"SEARCH\"],\n searchModuleType: \"TORZNAB\",\n state: \"ENABLED\",\n enabledForSearchSource: \"BOTH\"\n }\n ];\n\n $scope.emptyTorznabPreset = {\n allCapsChecked: false,\n configComplete: false,\n supportedSearchIds: undefined,\n supportedSearchTypes: undefined,\n searchModuleType: \"TORZNAB\",\n state: \"ENABLED\",\n enabledForSearchSource: \"BOTH\"\n };\n $scope.torznabPresets = _.sortBy($scope.torznabPresets, function (entry) {\n return entry.name.toLowerCase()\n });\n}]);\n\n\nangular.module('nzbhydraApp').controller('IndexerConfigBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$http\", \"model\", \"form\", \"fields\", \"isInitial\", \"parentModel\", \"growl\", \"IndexerCheckBeforeCloseService\", function ($scope, $q, $uibModalInstance, $http, model, form, fields, isInitial, parentModel, growl, IndexerCheckBeforeCloseService) {\n\n $scope.model = model;\n $scope.fields = fields;\n $scope.isInitial = isInitial;\n $scope.spinnerActive = false;\n $scope.needsConnectionTest = false;\n\n $scope.obSubmit = function () {\n if (model.searchModuleType === 'JACKETT_CONFIG') {\n $uibModalInstance.close(model);\n } else if (form.$valid) {\n var a = IndexerCheckBeforeCloseService.checkBeforeClose($scope, model).then(function (data) {\n if (angular.isDefined(data)) {\n $scope.model = data;\n }\n $uibModalInstance.close(data);\n });\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n angular.forEach(form.$error, function (error) {\n angular.forEach(error, function (field) {\n field.$setTouched();\n });\n });\n }\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.deleteEntry = function () {\n parentModel.splice(parentModel.indexOf(model), 1);\n $uibModalInstance.close($scope);\n };\n\n $scope.reset = function () {\n //Reset the model twice (for some reason when we do it once the search types / ids fields are empty, resetting again fixes that... (wtf))\n $scope.options.resetModel();\n $scope.options.resetModel();\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\n if (reason === \"backdrop click\") {\n $scope.reset($scope);\n }\n });\n}]);\n\n\nangular\n .module('nzbhydraApp')\n .controller('CheckCapsModalInstanceCtrl', CheckCapsModalInstanceCtrl);\n\nfunction CheckCapsModalInstanceCtrl($scope, $interval, $http, $timeout, growl, capsCheckRequest) {\n\n var updateMessagesInterval = undefined;\n\n $scope.messages = undefined;\n $http.post(\"internalapi/indexer/checkCaps\", capsCheckRequest).then(function (response) {\n $scope.$close([response.data, capsCheckRequest.indexerConfig]);\n if (response.data.length === 0) {\n growl.info(\"No indexers were checked\");\n }\n }, function () {\n $scope.$dismiss(\"Unknown error\")\n });\n\n $timeout(\n updateMessagesInterval = $interval(function () {\n $http.get(\"internalapi/indexer/checkCapsMessages\").then(function (response) {\n var map = response.data;\n var messages = [];\n for (var name in map) {\n if (map.hasOwnProperty(name)) {\n for (var i = 0; i < map[name].length; i++) {\n var message = \"\";\n if (capsCheckRequest.checkType !== \"SINGLE\") {\n message += name + \": \";\n }\n message += map[name][i];\n messages.push(message);\n }\n }\n }\n $scope.messages = messages;\n });\n\n }, 500),\n 500);\n\n\n $scope.$on('$destroy', function () {\n if (angular.isDefined(updateMessagesInterval)) {\n $interval.cancel(updateMessagesInterval);\n }\n });\n}\n\nangular\n .module('nzbhydraApp')\n .factory('IndexerConfigBoxService', IndexerConfigBoxService);\n\nfunction IndexerConfigBoxService($http, $q, $uibModal) {\n\n return {\n checkConnection: checkConnection,\n checkCaps: checkCaps\n };\n\n function checkConnection(url, settings) {\n var deferred = $q.defer();\n\n $http.post(url, settings).then(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click\n if (result.data.successful) {\n deferred.resolve({checked: true, message: null, model: result.data});\n } else {\n deferred.reject({checked: true, message: result.data.message});\n }\n }, function (result) {\n deferred.reject({checked: false, message: result.data.message});\n });\n\n return deferred.promise;\n }\n\n function checkCaps(capsCheckRequest) {\n var deferred = $q.defer();\n\n var result = $uibModal.open({\n templateUrl: 'static/html/checker-state.html',\n controller: CheckCapsModalInstanceCtrl,\n size: \"md\",\n backdrop: \"static\",\n backdropClass: \"waiting-cursor\",\n resolve: {\n capsCheckRequest: function () {\n return capsCheckRequest;\n }\n }\n });\n\n result.result.then(function (data) {\n deferred.resolve(data[0], data[1]);\n }, function (message) {\n deferred.reject(message);\n });\n\n return deferred.promise;\n }\n\n}\n\nangular\n .module('nzbhydraApp')\n .factory('IndexerCheckBeforeCloseService', IndexerCheckBeforeCloseService);\n\nfunction IndexerCheckBeforeCloseService($q, ModalService, IndexerConfigBoxService, growl, blockUI) {\n\n return {\n checkBeforeClose: checkBeforeClose\n };\n\n function checkBeforeClose(scope, model) {\n var deferred = $q.defer();\n if (model.searchModuleType === 'JACKETT_CONFIG') {\n deferred.resolve(model);\n } else if (!scope.isInitial && (!scope.needsConnectionTest || scope.form.capsChecked)) {\n checkCapsWhenClosing(scope, model).then(function () {\n deferred.resolve(model);\n }, function () {\n deferred.reject();\n });\n } else {\n scope.spinnerActive = true;\n blockUI.start(\"Testing connection...\");\n var url = \"internalapi/indexer/checkConnection\";\n IndexerConfigBoxService.checkConnection(url, model).then(function () {\n growl.info(\"Connection to the indexer tested successfully\");\n checkCapsWhenClosing(scope, model).then(function (data) {\n scope.spinnerActive = false;\n blockUI.reset();\n deferred.resolve(data);\n }, function () {\n scope.spinnerActive = false;\n blockUI.reset();\n deferred.reject();\n });\n },\n function (data) {\n scope.spinnerActive = false;\n blockUI.reset();\n handleConnectionCheckFail(ModalService, data, model, \"indexer\", deferred);\n });\n }\n return deferred.promise;\n }\n\n //Called when the indexer dialog is closed\n function checkCapsWhenClosing(scope, model) {\n var deferred = $q.defer();\n if (angular.isUndefined(model.supportedSearchIds) || angular.isUndefined(model.supportedSearchTypes)) {\n\n blockUI.start(\"New indexer found. Testing its capabilities. This may take a bit...\");\n IndexerConfigBoxService.checkCaps({indexerConfig: model, checkType: \"SINGLE\"}).then(\n function (data) {\n data = data[0]; //We get a list of results (with one result because the check type is single)\n blockUI.reset();\n scope.spinnerActive = false;\n if (data.allCapsChecked && data.configComplete) {\n growl.info(\"Successfully tested capabilites of indexer\");\n } else if (!data.allCapsChecked && data.configComplete) {\n ModalService.open(\"Incomplete caps check\", \"The capabilities of the indexer could not be checked completely. You may use it but it's recommended to repeat the check at another time.
          Until then some search types or IDs may not be usable.\", {}, \"md\", \"left\");\n } else if (!data.configComplete) {\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually from the indexer config box\", {}, \"md\", \"left\");\n }\n\n deferred.resolve(data.indexerConfig);\n },\n function () {\n blockUI.reset();\n scope.spinnerActive = false;\n model.supportedSearchIds = undefined;\n model.supportedSearchTypes = undefined;\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually using the button below.\", {}, \"md\", \"left\");\n deferred.resolve();\n }).finally(\n function () {\n scope.spinnerActive = false;\n })\n } else {\n deferred.resolve();\n }\n return deferred.promise;\n }\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nDownloaderConfigBoxService.$inject = [\"$http\", \"$q\", \"$uibModal\"];\nDownloaderCheckBeforeCloseService.$inject = [\"$q\", \"DownloaderConfigBoxService\", \"growl\", \"ModalService\", \"blockUI\"];\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n\n formlyConfigProvider.setType({\n name: 'downloaderConfig',\n templateUrl: 'static/html/config/downloader-config.html',\n controller: function ($scope, $uibModal, growl, CategoriesService, localStorageService) {\n $scope.formOptions = {formState: $scope.formState};\n $scope._showBox = _showBox;\n $scope.showBox = showBox;\n $scope.isInitial = false;\n $scope.presets = [\n {\n name: \"NZBGet\",\n downloaderType: \"NZBGET\",\n username: \"nzbgetx\",\n nzbAddingType: \"UPLOAD\",\n nzbAccessType: \"REDIRECT\",\n iconCssClass: \"\",\n downloadType: \"NZB\",\n url: \"http://nzbget:tegbzn6789@localhost:6789\"\n },\n {\n url: \"http://localhost:8080\",\n downloaderType: \"SABNZBD\",\n name: \"SABnzbd\",\n nzbAddingType: \"UPLOAD\",\n nzbAccessType: \"REDIRECT\",\n iconCssClass: \"\",\n downloadType: \"NZB\"\n }\n ];\n\n function _showBox(model, parentModel, isInitial, callback) {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/config/downloader-config-box.html',\n controller: 'DownloaderConfigBoxInstanceController',\n size: 'lg',\n resolve: {\n model: function () {\n //Isn't properly stored in parentmodel for some reason, this works just as well\n model.showAdvanced = localStorageService.get(\"showAdvanced\");\n console.log(model.showAdvanced);\n return model;\n },\n fields: function () {\n return getDownloaderBoxFields(model, parentModel, isInitial, angular.injector(), CategoriesService);\n },\n isInitial: function () {\n return isInitial\n },\n parentModel: function () {\n return parentModel;\n },\n data: function () {\n return $scope.options.data;\n }\n }\n });\n\n\n modalInstance.result.then(function (returnedModel) {\n $scope.form.$setDirty(true);\n if (angular.isDefined(callback)) {\n callback(true, returnedModel);\n }\n }, function () {\n if (angular.isDefined(callback)) {\n callback(false);\n }\n });\n }\n\n function showBox(model, parentModel) {\n $scope._showBox(model, parentModel, false)\n }\n\n $scope.addEntry = function (entriesCollection, preset) {\n var model = angular.copy({\n enabled: true\n });\n if (angular.isDefined(preset)) {\n _.extend(model, preset);\n }\n\n $scope.isInitial = true;\n\n $scope._showBox(model, entriesCollection, true, function (isSubmitted, returnedModel) {\n if (isSubmitted) {\n //Here is where the entry is actually added to the model\n entriesCollection.push(angular.isDefined(returnedModel) ? returnedModel : model);\n }\n });\n };\n\n function getDownloaderBoxFields(model, parentModel, isInitial) {\n var fieldset = [];\n\n fieldset = _.union(fieldset, [\n {\n key: 'enabled',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Enabled'\n }\n },\n {\n key: 'name',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Name',\n required: true\n },\n validators: {\n uniqueName: {\n expression: function (viewValue) {\n if (isInitial || viewValue !== model.name) {\n return _.pluck(parentModel, \"name\").indexOf(viewValue) === -1;\n }\n return true;\n },\n message: '\"Downloader \\\\\"\" + $viewValue + \"\\\\\" already exists\"'\n }\n }\n\n },\n {\n key: 'url',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'URL',\n help: 'URL with scheme and full path',\n required: true\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n }\n ]);\n\n\n if (model.downloaderType === \"SABNZBD\") {\n fieldset.push({\n key: 'apiKey',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'API Key'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n })\n } else if (model.downloaderType === \"NZBGET\") {\n fieldset.push({\n key: 'username',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Username'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n });\n fieldset.push({\n key: 'password',\n type: 'passwordSwitch',\n templateOptions: {\n type: 'text',\n label: 'Password'\n },\n watcher: {\n listener: function (field, newValue, oldValue, scope) {\n if (newValue !== oldValue) {\n scope.$parent.needsConnectionTest = true;\n }\n }\n }\n })\n }\n\n fieldset = _.union(fieldset, [\n {\n key: 'defaultCategory',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Default category',\n help: 'When adding NZBs this category will be used instead of asking for the category. Write \"Use original category\", \"Use no category\" or \"Use mapped category\" to not be asked.',\n placeholder: 'Ask when downloading'\n }\n },\n {\n key: 'nzbAddingType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'NZB adding type',\n options: [\n {name: 'Send link', value: 'SEND_LINK'},\n {name: 'Upload NZB', value: 'UPLOAD'}\n ],\n help: \"How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data.\",\n tooltip: 'You can select if you want to upload the NZB to the downloader or send a Hydra link. The downloader will do the download itself. This is a matter of taste, but adding a link and redirecting the downloader is the fastest way.' +\n '
          Usually the links are determined using the URL via which you call it in your browser. If your downloader cannot access NZBHydra using that URL you can set a specific URL to be used in the main downloading config.',\n advanced: true\n }\n },\n {\n key: 'addPaused',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Add paused',\n help: 'Add NZBs paused',\n advanced: true\n }\n },\n {\n key: 'iconCssClass',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Icon CSS class',\n help: 'Copy an icon name from https://fontawesome.com/v4.7.0/icons/ (e.g. \"film\")',\n placeholder: 'Default',\n tooltip: 'If you have multiple downloaders of the same type you can select an icon from the Font Awesome library. This icon will be shown in the search results and the NZB download history instead of the default downloader icon.',\n advanced: true\n }\n }\n ]);\n\n return fieldset;\n }\n }\n });\n }]);\n\n\nangular\n .module('nzbhydraApp')\n .factory('DownloaderConfigBoxService', DownloaderConfigBoxService);\n\nfunction DownloaderConfigBoxService($http, $q, $uibModal) {\n\n return {\n checkConnection: checkConnection,\n checkCaps: checkCaps\n };\n\n function checkConnection(url, settings) {\n var deferred = $q.defer();\n\n $http.post(url, settings).then(function (result) {\n //Using ng-class and a scope variable doesn't work for some reason, is only updated at second click\n if (result.data.successful) {\n deferred.resolve({checked: true, message: null, model: result.data});\n } else {\n deferred.reject({checked: true, message: result.data.message});\n }\n }, function (result) {\n deferred.reject({checked: false, message: result.data.message});\n });\n\n return deferred.promise;\n }\n\n function checkCaps(capsCheckRequest) {\n var deferred = $q.defer();\n\n var result = $uibModal.open({\n templateUrl: 'static/html/checker-state.html',\n controller: CheckCapsModalInstanceCtrl,\n size: \"md\",\n backdrop: \"static\",\n backdropClass: \"waiting-cursor\",\n resolve: {\n capsCheckRequest: function () {\n return capsCheckRequest;\n }\n }\n });\n\n result.result.then(function (data) {\n deferred.resolve(data[0], data[1]);\n }, function (message) {\n deferred.reject(message);\n });\n\n return deferred.promise;\n }\n}\n\nangular.module('nzbhydraApp').controller('DownloaderConfigBoxInstanceController', [\"$scope\", \"$q\", \"$uibModalInstance\", \"$http\", \"model\", \"fields\", \"isInitial\", \"parentModel\", \"data\", \"growl\", \"DownloaderCheckBeforeCloseService\", function ($scope, $q, $uibModalInstance, $http, model, fields, isInitial, parentModel, data, growl, DownloaderCheckBeforeCloseService) {\n\n $scope.model = model;\n $scope.fields = fields;\n $scope.isInitial = isInitial;\n $scope.spinnerActive = false;\n $scope.needsConnectionTest = false;\n\n $scope.obSubmit = function () {\n if ($scope.form.$valid) {\n var a = DownloaderCheckBeforeCloseService.checkBeforeClose($scope, model).then(function (data) {\n if (angular.isDefined(data)) {\n $scope.model = data;\n }\n $uibModalInstance.close(data);\n });\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n angular.forEach($scope.form.$error, function (error) {\n angular.forEach(error, function (field) {\n field.$setTouched();\n });\n });\n }\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.deleteEntry = function () {\n parentModel.splice(parentModel.indexOf(model), 1);\n $uibModalInstance.close($scope);\n };\n\n $scope.reset = function () {\n if (angular.isDefined(data.resetFunction)) {\n //Reset the model twice (for some reason when we do it once the search types / ids fields are empty, resetting again fixes that... (wtf))\n $scope.options.resetModel();\n $scope.options.resetModel();\n }\n };\n\n $scope.$on(\"modal.closing\", function (targetScope, reason) {\n if (reason === \"backdrop click\") {\n $scope.reset($scope);\n }\n });\n}]);\n\n\nangular\n .module('nzbhydraApp')\n .factory('DownloaderCheckBeforeCloseService', DownloaderCheckBeforeCloseService);\n\nfunction DownloaderCheckBeforeCloseService($q, DownloaderConfigBoxService, growl, ModalService, blockUI) {\n\n return {\n checkBeforeClose: checkBeforeClose\n };\n\n function checkBeforeClose(scope, model) {\n var deferred = $q.defer();\n if (!scope.isInitial && !scope.needsConnectionTest) {\n deferred.resolve();\n } else {\n scope.spinnerActive = true;\n blockUI.start(\"Testing connection...\");\n var url = \"internalapi/downloader/checkConnection\";\n DownloaderConfigBoxService.checkConnection(url, JSON.stringify(model)).then(function () {\n blockUI.reset();\n scope.spinnerActive = false;\n growl.info(\"Connection to the downloader tested successfully\");\n deferred.resolve();\n },\n function (data) {\n blockUI.reset();\n scope.spinnerActive = false;\n handleConnectionCheckFail(ModalService, data, model, \"downloader\", deferred);\n }).finally(function () {\n scope.spinnerActive = false;\n blockUI.reset();\n });\n }\n return deferred.promise;\n }\n}","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nhashCode = function (s) {\n return s.split(\"\").reduce(function (a, b) {\n a = ((a << 5) - a) + b.charCodeAt(0);\n return a & a\n }, 0);\n};\n\nangular\n .module('nzbhydraApp').run([\"formlyConfig\", \"formlyValidationMessages\", function (formlyConfig, formlyValidationMessages) {\n formlyValidationMessages.addStringMessage('required', 'This field is required');\n formlyValidationMessages.addStringMessage('newznabCategories', 'Invalid');\n formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'fc.$touched || form.$submitted';\n}]);\n\nangular\n .module('nzbhydraApp')\n .config([\"formlyConfigProvider\", function config(formlyConfigProvider) {\n formlyConfigProvider.extras.removeChromeAutoComplete = true;\n formlyConfigProvider.extras.explicitAsync = true;\n formlyConfigProvider.disableWarnings = window.onProd;\n\n\n formlyConfigProvider.setWrapper({\n name: 'settingWrapper',\n templateUrl: 'setting-wrapper.html'\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'fieldset',\n templateUrl: 'fieldset-wrapper.html',\n controller: ['$scope', function ($scope) {\n $scope.tooltipIsOpen = false;\n }]\n });\n\n formlyConfigProvider.setType({\n name: 'help',\n template: [\n '
          ',\n '
          ',\n '
          ',\n '
          {{ line | derefererExtracting | unsafe }}
          ',\n '
          ',\n '
          ',\n '
          '\n ].join(' ')\n });\n\n\n formlyConfigProvider.setWrapper({\n name: 'logicalGroup',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalInput',\n extends: 'input',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalTextArea',\n extends: 'textarea',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'timeOfDay',\n extends: 'horizontalInput',\n controller: ['$scope', function ($scope) {\n $scope.model[$scope.options.key] = moment.utc($scope.model[$scope.options.key]).toDate();\n }]\n });\n\n formlyConfigProvider.setType({\n name: 'passwordSwitch',\n extends: 'horizontalInput',\n template: [\n '
          ',\n '',\n '',\n '',\n '
          '\n ].join(' '),\n controller: function ($scope) {\n $scope.hidePassword = true;\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalChips',\n extends: 'horizontalInput',\n template: '' +\n ' ' +\n '
          ' +\n ' {{chip}}' +\n ' ' +\n '
          ' +\n '
          ' +\n ' ' +\n '
          '\n });\n\n formlyConfigProvider.setType({\n name: 'percentInput',\n template: [\n ''\n ].join(' ')\n });\n\n formlyConfigProvider.setType({\n name: 'apiKeyInput',\n template: [\n '
          ',\n '',\n '',\n '',\n '
          '\n ].join(' '),\n controller: function ($scope) {\n $scope.generate = function () {\n var result = \"\";\n var length = 24;\n var chars = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n for (var i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];\n $scope.model[$scope.options.key] = result;\n $scope.form.$setDirty(true);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'fileInput',\n extends: 'horizontalInput',\n template: [\n '
          ',\n '',\n '',\n '',\n '
          '\n ].join(' '),\n controller: function ($scope, FileSelectionService) {\n $scope.open = function () {\n FileSelectionService.open($scope.model[$scope.options.key], $scope.to.type).then(function (selection) {\n $scope.model[$scope.options.key] = selection;\n });\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'colorInput',\n extends: 'horizontalInput',\n templateUrl: 'static/html/config/color-control.html',\n controller: function ($scope) {\n //Model format: rgb(116,18,18)\n //Input format: rgba(100,42,41,0.5)\n if (!_.isNullOrEmpty($scope.model.color)) {\n $scope.color = $scope.model.color;\n }\n $scope.convertColorToCss = function () {\n if (_.isNullOrEmpty($scope.model.color)) {\n return \"\";\n }\n return $scope.model.color.replace(\"rgb\", \"rgba\").replace(\")\", \",0.5)\");\n }\n $scope.convertColorFromInput = function () {\n if (_.isNullOrEmpty($scope.color)) {\n return;\n }\n $scope.model.color = $scope.color.replace(\"rgba\", \"rgb\").replace(\",0.5)\", \")\");\n }\n $scope.clear = function () {\n $scope.model.color = null;\n $scope.color = null;\n }\n $scope.$watch(\"model.color\", function () {\n if (!_.isNullOrEmpty($scope.model.color)) {\n $scope.color = $scope.model.color;\n }\n })\n }\n });\n\n formlyConfigProvider.setType({\n name: 'testConnection',\n templateUrl: 'button-test-connection.html'\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalTestConnection',\n extends: 'testConnection',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'customMappingTest',\n extends: 'horizontalInput',\n template: [\n '
          ',\n '',\n '
          '\n ].join(' '),\n controller: function ($scope, $uibModal, $http) {\n var model = $scope.model;\n var modelCopy = Object.assign({}, model);\n $scope.open = function () {\n $uibModal.open({\n templateUrl: 'static/html/custom-mapping-help.html',\n controller: [\"$scope\", \"$uibModalInstance\", \"$http\", function ($scope, $uibModalInstance, $http) {\n $scope.model = modelCopy;\n $scope.cancel = function () {\n $uibModalInstance.close();\n }\n $scope.submit = function () {\n Object.assign(model, $scope.model)\n $uibModalInstance.close();\n }\n\n $scope.test = function () {\n if (!$scope.exampleInput) {\n $scope.exampleResult = \"Empty example data\";\n return;\n\n }\n $http.post('internalapi/customMapping/test', {mapping: model, exampleInput: $scope.exampleInput}).then(function (response) {\n console.log(response.data);\n console.log(response.data.output);\n if (response.data.error) {\n $scope.exampleResult = response.data.error;\n } else if (response.data.match) {\n $scope.exampleResult = response.data.output;\n } else {\n $scope.exampleResult = \"Input does not match example\";\n }\n }, function (response) {\n $scope.exampleResult = response.message;\n })\n }\n }],\n size: \"md\"\n })\n }\n }\n });\n\n function updateIndexerModel(model, indexerConfig) {\n model.supportedSearchIds = indexerConfig.supportedSearchIds;\n model.supportedSearchTypes = indexerConfig.supportedSearchTypes;\n model.categoryMapping = indexerConfig.categoryMapping;\n model.configComplete = indexerConfig.configComplete;\n model.allCapsChecked = indexerConfig.allCapsChecked;\n model.hitLimit = indexerConfig.hitLimit;\n model.downloadLimit = indexerConfig.downloadLimit;\n model.state = indexerConfig.state;\n }\n\n formlyConfigProvider.setType({\n //BUtton\n name: 'checkCaps',\n templateUrl: 'button-check-caps.html',\n controller: function ($scope, IndexerConfigBoxService, ModalService, growl) {\n $scope.message = \"\";\n $scope.uniqueId = hashCode($scope.model.name) + hashCode($scope.model.host);\n\n var testButton = \"#button-check-caps-\" + $scope.uniqueId;\n var testMessage = \"#message-check-caps-\" + $scope.uniqueId;\n\n function showSuccess() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).removeClass(\"btn-warning\");\n angular.element(testButton).addClass(\"btn-success\");\n }\n\n function showError() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-warning\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-danger\");\n }\n\n function showWarning() {\n angular.element(testButton).removeClass(\"btn-default\");\n angular.element(testButton).removeClass(\"btn-danger\");\n angular.element(testButton).removeClass(\"btn-success\");\n angular.element(testButton).addClass(\"btn-warning\");\n }\n\n\n //When button is clicked\n $scope.checkCaps = function () {\n angular.element(testButton).addClass(\"glyphicon-refresh-animate\");\n IndexerConfigBoxService.checkCaps({\n indexerConfig: $scope.model,\n checkType: \"SINGLE\"\n }).then(function (data) {\n data = data[0]; //We get a list of results (with one result because the check type is single)\n //Formly doesn't allow replacing the model so we need to set all the relevant values ourselves\n updateIndexerModel($scope.model, data.indexerConfig);\n if (data.indexerConfig.supportedSearchIds.length > 0) {\n var message = \"Supports \" + data.indexerConfig.supportedSearchIds;\n angular.element(testMessage).text(message);\n }\n if (data.indexerConfig.allCapsChecked && data.indexerConfig.configComplete) {\n showSuccess();\n growl.info(\"Successfully tested capabilites of indexer\");\n $scope.form.capsChecked = true;\n } else if (!data.indexerConfig.allCapsChecked && data.indexerConfig.configComplete) {\n showWarning();\n ModalService.open(\"Incomplete caps check\", \"The capabilities of the indexer could not be checked completely. You may use it but it's recommended to repeat the check at another time.
          Until then some search types or IDs may not be usable.\", {}, \"md\", \"left\");\n $scope.form.capsChecked = true;\n } else if (!data.configComplete) {\n showError();\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually from the indexer config box\", {}, \"md\", \"left\");\n }\n }, function (message) {\n angular.element(testMessage).text(message);\n showError();\n ModalService.open(\"Error testing capabilities\", \"An error occurred while contacting the indexer. It will not be usable until the caps check has been executed. You can trigger it manually from the indexer config box\", {}, \"md\", \"left\");\n }).finally(function () {\n angular.element(testButton).removeClass(\"glyphicon-refresh-animate\");\n });\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalCheckCaps',\n extends: 'checkCaps',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalApiKeyInput',\n extends: 'apiKeyInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalPercentInput',\n extends: 'percentInput',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'switch',\n template: '
          '\n });\n\n formlyConfigProvider.setType({\n name: 'indexerStateSwitch',\n template: ''\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalIndexerStateSwitch',\n extends: 'indexerStateSwitch',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n formlyConfigProvider.setType({\n name: 'duoSetting',\n extends: 'input',\n defaultOptions: {\n className: 'col-md-9',\n templateOptions: {\n type: 'number',\n noRow: true,\n label: ''\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSwitch',\n extends: 'switch',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalSelect',\n extends: 'select',\n wrapper: ['settingWrapper', 'bootstrapHasError'],\n controller: function ($scope) {\n if ($scope.options.templateOptions.optionsFunction !== undefined) {\n $scope.to.options.push.apply($scope.to.options, $scope.options.templateOptions.optionsFunction($scope.model));\n }\n if ($scope.options.templateOptions.optionsFunctionAfter !== undefined) {\n $scope.options.templateOptions.optionsFunctionAfter($scope.model);\n }\n }\n });\n\n\n formlyConfigProvider.setType({\n name: 'horizontalMultiselect',\n defaultOptions: {\n templateOptions: {\n optionsAttr: 'bs-options',\n ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search'\n }\n },\n template: '',\n controller: function ($scope) {\n var settings = $scope.to.settings || [];\n settings.classes = settings.classes || [];\n angular.extend(settings.classes, [\"form-control\"]);\n $scope.settings = settings;\n if ($scope.options.templateOptions.optionsFunction !== null && $scope.options.templateOptions.optionsFunction !== undefined) {\n $scope.to.options.push.apply($scope.to.options, $scope.options.templateOptions.optionsFunction($scope.model));\n }\n $scope.events = {\n onToggleItem: function (item, newValue) {\n $scope.form.$setDirty(true);\n }\n }\n },\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n formlyConfigProvider.setType({\n name: 'label',\n template: ''\n });\n\n formlyConfigProvider.setType({\n name: 'duolabel',\n extends: 'label',\n defaultOptions: {\n className: 'col-md-2',\n templateOptions: {\n label: '-'\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'repeatSection',\n templateUrl: 'repeatSection.html',\n controller: function ($scope) {\n $scope.formOptions = {formState: $scope.formState};\n $scope.addNew = addNew;\n $scope.remove = remove;\n $scope.copyFields = copyFields;\n\n function copyFields(fields) {\n fields = angular.copy(fields);\n $scope.repeatfields = fields;\n return fields;\n }\n\n $scope.clear = function (field) {\n return _.mapObject(field, function (key, val) {\n if (typeof val === 'object') {\n return $scope.clear(val);\n }\n return undefined;\n\n });\n };\n\n function addNew(preset) {\n console.log(preset);\n $scope.form.$setDirty(true);\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\n var repeatsection = $scope.model[$scope.options.key];\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\n Object.assign(newsection, preset);\n repeatsection.push(newsection);\n }\n\n function remove($index) {\n $scope.model[$scope.options.key].splice($index, 1);\n $scope.form.$setDirty(true);\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'recheckAllCaps',\n templateUrl: 'static/html/config/recheck-all-caps.html',\n controller: function ($scope, $uibModal, growl, IndexerConfigBoxService) {\n $scope.recheck = function (checkType) {\n IndexerConfigBoxService.checkCaps({checkType: checkType}).then(function (listOfResults) {\n //A bit ugly, but we have to update the current model with the new data from the list\n for (var i = 0; i < $scope.model.length; i++) {\n for (var j = 0; j < listOfResults.length; j++) {\n if ($scope.model[i].name === listOfResults[j].indexerConfig.name) {\n updateIndexerModel($scope.model[i], listOfResults[j].indexerConfig);\n $scope.form.$setDirty(true);\n }\n }\n }\n });\n }\n }\n });\n\n\n formlyConfigProvider.setType({\n name: 'notificationSection',\n templateUrl: 'notificationRepeatSection.html',\n controller: function ($scope, NotificationService) {\n $scope.formOptions = {formState: $scope.formState};\n $scope.addNew = addNew;\n $scope.remove = remove;\n $scope.copyFields = copyFields;\n $scope.eventTypes = [];\n\n var allData = NotificationService.getAllData();\n _.each(_.keys(allData), function (key) {\n $scope.eventTypes.push({\"key\": key, \"label\": allData[key].readable})\n })\n\n function copyFields(fields) {\n fields = angular.copy(fields);\n $scope.repeatfields = fields;\n return fields;\n }\n\n $scope.clear = function (field) {\n return _.mapObject(field, function (key, val) {\n if (typeof val === 'object') {\n return $scope.clear(val);\n }\n return undefined;\n\n });\n };\n\n function addNew(eventType) {\n $scope.form.$setDirty(true);\n $scope.model[$scope.options.key] = $scope.model[$scope.options.key] || [];\n var repeatsection = $scope.model[$scope.options.key];\n var newsection = angular.copy($scope.options.templateOptions.defaultModel);\n\n var eventTypeData = NotificationService.getAllData()[eventType];\n console.log(eventTypeData);\n newsection.eventType = eventType;\n newsection.titleTemplate = eventTypeData.titleTemplate;\n newsection.bodyTemplate = eventTypeData.bodyTemplate;\n newsection.messageType = eventTypeData.messageType;\n\n repeatsection.push(newsection);\n }\n\n function remove($index) {\n $scope.model[$scope.options.key].splice($index, 1);\n $scope.form.$setDirty(true);\n }\n }\n });\n\n formlyConfigProvider.setType({\n //Button\n name: 'testNotification',\n templateUrl: 'button-test-notification.html',\n controller: function ($scope, NotificationService) {\n\n\n //When button is clicked\n $scope.testNotification = function () {\n NotificationService.testNotification($scope.model.eventType)\n }\n }\n });\n\n formlyConfigProvider.setType({\n name: 'horizontalTestNotification',\n extends: 'testNotification',\n wrapper: ['settingWrapper', 'bootstrapHasError']\n });\n\n\n }]);\n\n","\nConfigService.$inject = [\"$http\", \"$q\", \"$cacheFactory\", \"$uibModal\", \"bootstrapped\", \"RequestsErrorHandler\"];angular\n .module('nzbhydraApp')\n .factory('ConfigService', ConfigService);\n\nfunction ConfigService($http, $q, $cacheFactory, $uibModal, bootstrapped, RequestsErrorHandler) {\n\n ConfigureInModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$http\", \"growl\", \"$interval\", \"RequestsErrorHandler\", \"localStorageService\", \"externalTool\", \"dialogInfo\"];\n var cache = $cacheFactory(\"nzbhydra\");\n var safeConfig = bootstrapped.safeConfig;\n\n return {\n set: set,\n get: get,\n getSafe: getSafe,\n invalidateSafe: invalidateSafe,\n maySeeAdminArea: maySeeAdminArea,\n reloadConfig: reloadConfig,\n apiHelp: apiHelp,\n configureIn: configureIn\n };\n\n function set(newConfig, ignoreWarnings) {\n var deferred = $q.defer();\n $http.put('internalapi/config', newConfig)\n .then(function (response) {\n if (response.data.ok && (ignoreWarnings || response.data.warningMessages.length === 0)) {\n cache.put(\"config\", newConfig);\n setTimeout(function () {\n invalidateSafe();\n }, 500)\n }\n deferred.resolve(response);\n\n }, function (errorresponse) {\n console.log(\"Error saving settings:\");\n console.log(errorresponse);\n deferred.reject(errorresponse);\n });\n return deferred.promise;\n }\n\n function reloadConfig() {\n return $http.get('internalapi/config/reload').then(function (response) {\n return response.data;\n });\n }\n\n function apiHelp() {\n return $http.get('internalapi/config/apiHelp').then(function (response) {\n return response.data;\n });\n }\n\n function get() {\n var config = cache.get(\"config\");\n if (angular.isUndefined(config)) {\n config = $http.get('internalapi/config').then(function (response) {\n return response.data;\n });\n cache.put(\"config\", config);\n }\n\n return config;\n }\n\n function getSafe() {\n return safeConfig;\n }\n\n function invalidateSafe() {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get('internalapi/config/safe').then(function (response) {\n safeConfig = response.data;\n });\n });\n\n }\n\n function maySeeAdminArea() {\n function loadAll() {\n var maySeeAdminArea = cache.get(\"maySeeAdminArea\");\n if (!angular.isUndefined(maySeeAdminArea)) {\n var deferred = $q.defer();\n deferred.resolve(maySeeAdminArea);\n return deferred.promise;\n }\n\n return $http.get('internalapi/mayseeadminarea')\n .then(function (configResponse) {\n var config = configResponse.data;\n cache.put(\"maySeeAdminArea\", config);\n return configResponse.data;\n });\n }\n\n return loadAll().then(function (maySeeAdminArea) {\n return maySeeAdminArea;\n });\n }\n\n function configureIn(externalTool) {\n $uibModal.open({\n templateUrl: 'static/html/configure-in-modal.html',\n controller: ConfigureInModalInstanceCtrl,\n size: \"md\",\n resolve: {\n externalTool: function () {\n return externalTool;\n },\n dialogInfo: function () {\n return $http.get(\"internalapi/externalTools/getDialogInfo\").then(function (response) {\n return response.data;\n })\n }\n }\n })\n }\n\n function ConfigureInModalInstanceCtrl($scope, $uibModalInstance, $http, growl, $interval, RequestsErrorHandler, localStorageService, externalTool, dialogInfo) {\n var lastConfig = localStorageService.get(externalTool);\n\n $scope.externalTool = externalTool;\n $scope.externalToolDisplayName = externalTool;\n $scope.externalToolsMessages = [];\n $scope.closeButtonType = \"warning\";\n $scope.completed = false;\n $scope.working = false;\n $scope.showMessages = false;\n\n $scope.nzbhydraHost = dialogInfo.nzbhydraHost;\n $scope.usenetIndexersConfigured = dialogInfo.usenetIndexersConfigured;\n $scope.prioritiesConfigured = dialogInfo.prioritiesConfigured;\n $scope.configureForUsenet = dialogInfo.usenetIndexersConfigured;\n $scope.torrentIndexersConfigured = dialogInfo.torrentIndexersConfigured;\n $scope.configureForTorrents = dialogInfo.torrentIndexersConfigured;\n $scope.addDisabledIndexers = false;\n\n if (!$scope.configureForUsenet && !$scope.configureForTorrents) {\n growl.error(\"No usenet or torrent indexers configured\");\n }\n\n $scope.nzbhydraName = \"NZBHydra2\";\n $scope.xdarrHost = \"http://localhost:\"\n $scope.addType = \"SINGLE\";\n $scope.enableRss = true;\n $scope.enableAutomaticSearch = true;\n $scope.enableInteractiveSearch = true;\n $scope.categories = null;\n $scope.animeCategories = null;\n $scope.priority = 0;\n $scope.useHydraPriorities = true;\n\n if (externalTool === \"Sonarr\" || externalTool === \"Sonarrv3\") {\n $scope.xdarrHost += \"8989\";\n $scope.categories = \"5030,5040\";\n if (externalTool === \"Sonarrv3\") {\n $scope.externalToolDisplayName = \"Sonarr v3\";\n }\n } else if (externalTool === \"Radarr\" || externalTool === \"Radarrv3\") {\n $scope.xdarrHost += \"7878\";\n $scope.categories = \"2000\";\n if (externalTool === \"Radarrv3\") {\n $scope.externalToolDisplayName = \"Radarr v3+\";\n }\n } else if (externalTool === \"Lidarr\") {\n $scope.xdarrHost += \"8686\";\n $scope.categories = \"3000\";\n } else if (externalTool === \"Readarr\") {\n $scope.xdarrHost += \"8787\";\n $scope.categories = \"7020,8010\";\n }\n $scope.removeYearFromSearchString = false;\n\n if (lastConfig !== null && lastConfig !== undefined) {\n Object.assign($scope, lastConfig);\n }\n\n $scope.close = function () {\n $uibModalInstance.dismiss();\n };\n\n $scope.submit = function (deleteOnly) {\n if ($scope.completed && !deleteOnly) {\n $uibModalInstance.dismiss();\n }\n if (!$scope.usenetIndexersConfigured && !$scope.torrentIndexersConfigured && !deleteOnly) {\n growl.error(\"No usenet or torrent indexers configured\");\n return;\n }\n $scope.externalToolsMessages = [];\n $scope.spinnerActive = true;\n $scope.working = true;\n $scope.showMessages = true;\n var data = {\n\n nzbhydraName: $scope.nzbhydraName,\n externalTool: $scope.externalTool,\n nzbhydraHost: $scope.nzbhydraHost,\n addType: deleteOnly ? \"DELETE_ONLY\" : $scope.addType,\n xdarrHost: $scope.xdarrHost,\n xdarrApiKey: $scope.xdarrApiKey,\n enableRss: $scope.enableRss,\n enableAutomaticSearch: $scope.enableAutomaticSearch,\n enableInteractiveSearch: $scope.enableInteractiveSearch,\n categories: $scope.categories,\n animeCategories: $scope.animeCategories,\n removeYearFromSearchString: $scope.removeYearFromSearchString,\n earlyDownloadLimit: $scope.earlyDownloadLimit,\n multiLanguages: $scope.multiLanguages,\n configureForUsenet: $scope.configureForUsenet,\n configureForTorrents: $scope.configureForTorrents,\n additionalParameters: $scope.additionalParameters,\n minimumSeeders: $scope.minimumSeeders,\n seedRatio: $scope.seedRatio,\n seedTime: $scope.seedTime,\n seasonPackSeedTime: $scope.seasonPackSeedTime,\n discographySeedTime: $scope.discographySeedTime,\n addDisabledIndexers: $scope.addDisabledIndexers,\n priority: $scope.priority,\n useHydraPriorities: $scope.useHydraPriorities\n }\n\n localStorageService.set(externalTool, data);\n\n function updateMessages() {\n $http.get(\"internalapi/externalTools/messages\").then(function (response) {\n $scope.externalToolsMessages = response.data;\n });\n }\n\n var updateInterval = $interval(function () {\n updateMessages();\n }, 500);\n\n RequestsErrorHandler.specificallyHandled(function () {\n $scope.completed = false;\n $http.post(\"internalapi/externalTools/configure\", data).then(function (response) {\n updateMessages();\n $interval.cancel(updateInterval);\n $scope.spinnerActive = false;\n console.log(response);\n if (response.data) {\n $scope.completed = true;\n $scope.closeButtonType = \"success\";\n } else {\n $scope.working = false;\n $scope.completed = false;\n }\n }, function (error) {\n updateMessages();\n console.error(error.data);\n $interval.cancel(updateInterval);\n $scope.completed = false;\n $scope.spinnerActive = false;\n $scope.working = false;\n });\n });\n };\n\n }\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nConfigFields.$inject = [\"$injector\"];\nangular\n .module('nzbhydraApp')\n .factory('ConfigFields', ConfigFields);\n\nfunction ConfigFields($injector) {\n return {\n getFields: getFields\n };\n\n function ipValidator() {\n return {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n return /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(value)\n || /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(value);\n }\n return true;\n },\n message: '$viewValue + \" is not a valid IP Address\"'\n };\n }\n\n function regexValidator(regex, message, prefixViewValue, preventEmpty) {\n return {\n expression: function ($viewValue, $modelValue) {\n var value = $modelValue || $viewValue;\n if (value) {\n if (Array.isArray(value)) {\n for (var i = 0; i < value.length; i++) {\n if (!regex.test(value[i])) {\n return false;\n }\n }\n return true;\n } else {\n return regex.test(value);\n }\n }\n return !preventEmpty;\n },\n message: (prefixViewValue ? '$viewValue + \" ' : '\" ') + message + '\"'\n };\n }\n\n function getFields(rootModel, showAdvanced) {\n return {\n main: [\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Hosting'},\n fieldGroup: [\n {\n key: 'host',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Host',\n required: true,\n placeholder: 'IPv4 address to bind to',\n help: 'I strongly recommend using a reverse proxy instead of exposing this directly. Requires restart.'\n },\n validators: {\n ipAddress: ipValidator()\n }\n },\n {\n key: 'port',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Port',\n required: true,\n placeholder: '5076',\n help: 'Requires restart.'\n },\n validators: {\n port: regexValidator(/^\\d{1,5}$/, \"is no valid port\", true)\n }\n },\n {\n key: 'urlBase',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'URL base',\n placeholder: '/nzbhydra',\n help: 'Adapt when using a reverse proxy. See wiki. Always use when calling Hydra, even locally.',\n tooltip: 'If you use Hydra behind a reverse proxy you might want to set the URL base to a value like \"/nzbhydra\". If you accesses Hydra with tools running outside your network (for example from your phone) set the external URL so that it matches the full Hydra URL. That way the NZB links returned in the search results refer to your global URL and not your local address.',\n advanced: true\n },\n validators: {\n urlBase: regexValidator(/^((\\/.*[^\\/])|\\/)$/, 'URL base has to start and may not end with /', false, true)\n }\n\n },\n {\n key: 'ssl',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Use SSL',\n help: 'Requires restart.',\n tooltip: 'You can use SSL but I recommend using a reverse proxy with SSL. See the wiki for notes regarding reverse proxies and SSL. It\\'s more secure and can be configured better.',\n advanced: true\n }\n },\n {\n key: 'sslKeyStore',\n hideExpression: '!model.ssl',\n type: 'fileInput',\n templateOptions: {\n label: 'SSL keystore file',\n required: true,\n type: \"file\",\n help: 'Requires restart. See wiki.'\n }\n },\n {\n key: 'sslKeyStorePassword',\n hideExpression: '!model.ssl',\n type: 'horizontalInput',\n templateOptions: {\n type: 'password',\n label: 'SSL keystore password',\n required: true,\n help: 'Requires restart.'\n }\n }\n\n\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Proxy',\n tooltip: 'You can select to use either a SOCKS or an HTTPS proxy. All outside connections will be done via the configured proxy.',\n advanced: true\n }\n ,\n fieldGroup: [\n {\n key: 'proxyType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Use proxy',\n options: [\n {name: 'None', value: 'NONE'},\n {name: 'SOCKS', value: 'SOCKS'},\n {name: 'HTTP(S)', value: 'HTTP'}\n ]\n }\n },\n {\n key: 'proxyHost',\n type: 'horizontalInput',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n label: 'SOCKS proxy host',\n placeholder: 'Set to use a SOCKS proxy',\n help: \"IPv4 only\"\n }\n },\n {\n key: 'proxyPort',\n type: 'horizontalInput',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'number',\n label: 'Proxy port',\n placeholder: '1080'\n }\n },\n {\n key: 'proxyUsername',\n type: 'horizontalInput',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n label: 'Proxy username'\n }\n },\n {\n key: 'proxyPassword',\n type: 'passwordSwitch',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n label: 'Proxy password'\n }\n },\n {\n key: 'proxyIgnoreLocal',\n type: 'horizontalSwitch',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'switch',\n label: 'Bypass local network addresses'\n }\n },\n {\n key: 'proxyIgnoreDomains',\n type: 'horizontalChips',\n hideExpression: 'model.proxyType===\"NONE\"',\n templateOptions: {\n type: 'text',\n help: 'Separate by comma. You can use wildcards (*). Case insensitive. Apply values with enter key.',\n label: 'Bypass domains'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'UI'},\n fieldGroup: [\n\n {\n key: 'theme',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Theme',\n options: [\n {name: 'Grey', value: 'grey'},\n {name: 'Bright', value: 'bright'},\n {name: 'Dark', value: 'dark'}\n ]\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Security'},\n fieldGroup: [\n {\n key: 'apiKey',\n type: 'horizontalApiKeyInput',\n templateOptions: {\n label: 'API key',\n help: 'Alphanumeric only.',\n required: true\n },\n validators: {\n apiKey: regexValidator(/^[a-zA-Z0-9]*$/, \"API key must only contain numbers and digits\", false)\n }\n },\n {\n key: 'dereferer',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Dereferer',\n help: 'Redirect external links to hide your instance. Insert $s for escaped target URL and $us for unescaped target URL. Use empty value to disable.',\n advanced: true\n }\n },\n {\n key: 'verifySsl',\n type: 'horizontalSwitch',\n templateOptions: {\n label: 'Verify SSL certificates',\n help: 'If enabled only valid/known SSL certificates will be accepted when accessing indexers. Change requires restart. See wiki.',\n advanced: true\n }\n },\n {\n key: 'verifySslDisabledFor',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Disable SSL for...',\n help: 'Add hosts for which to disable SSL verification. Apply words with return key.',\n advanced: true\n }\n },\n {\n key: 'disableSslLocally',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'text',\n label: 'Disable SSL locally',\n help: 'Disable SSL for local hosts.',\n advanced: true\n }\n },\n {\n key: 'sniDisabledFor',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Disable SNI',\n help: 'Add a host if you get an \"unrecognized_name\" error. Apply words with return key. See wiki.',\n advanced: true\n }\n },\n {\n key: 'useCsrf',\n type: 'horizontalSwitch',\n templateOptions: {\n label: 'Use CSRF protection',\n help: 'Use CSRF protection.',\n advanced: true\n }\n }\n ]\n },\n\n {\n wrapper: 'fieldset',\n key: 'logging',\n templateOptions: {\n label: 'Logging',\n tooltip: 'The base settings should suffice for most users. If you want you can enable logging of IP adresses for failed logins and NZB downloads.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'logfilelevel',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Logfile level',\n options: [\n {name: 'Error', value: 'ERROR'},\n {name: 'Warning', value: 'WARN'},\n {name: 'Info', value: 'INFO'},\n {name: 'Debug', value: 'DEBUG'}\n ],\n help: 'Takes effect on next restart.'\n }\n },\n {\n key: 'logMaxHistory',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Max log history',\n help: 'How many daily log files will be kept.'\n }\n },\n {\n key: 'consolelevel',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Console log level',\n options: [\n {name: 'Error', value: 'ERROR'},\n {name: 'Warning', value: 'WARN'},\n {name: 'Info', value: 'INFO'},\n {name: 'Debug', value: 'DEBUG'}\n ],\n help: 'Takes effect on next restart.'\n }\n },\n {\n key: 'logGc',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Log GC',\n help: 'Enable garbage collection logging. Only for debugging of memory issues.'\n }\n },\n {\n key: 'logIpAddresses',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Log IP addresses'\n }\n },\n {\n key: 'mapIpToHost',\n type: 'horizontalSwitch',\n hideExpression: '!model.logIpAddresses',\n templateOptions: {\n type: 'switch',\n label: 'Map hosts',\n help: 'Try to map logged IP addresses to host names.',\n tooltip: 'Enabling this may cause NZBHydra to load very, very slowly when accessed remotely.'\n }\n },\n {\n key: 'logUsername',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Log user names'\n }\n },\n {\n key: 'markersToLog',\n type: 'horizontalMultiselect',\n hideExpression: 'model.consolelevel !== \"DEBUG\" && model.logfilelevel !== \"DEBUG\"',\n templateOptions: {\n label: 'Log markers',\n help: 'Select certain sections for more output on debug level. Please enable only when asked for.',\n options: [\n {label: 'API limits', id: 'LIMITS'},\n {label: 'Category mapping', id: 'CATEGORY_MAPPING'},\n {label: 'Config file handling', id: 'CONFIG_READ_WRITE'},\n {label: 'Custom mapping', id: 'CUSTOM_MAPPING'},\n {label: 'Downloader status updating', id: 'DOWNLOADER_STATUS_UPDATE'},\n {label: 'Duplicate detection', id: 'DUPLICATES'},\n {label: 'External tool configuration', id: 'EXTERNAL_TOOLS'},\n {label: 'History cleanup', id: 'HISTORY_CLEANUP'},\n {label: 'HTTP', id: 'HTTP'},\n {label: 'HTTPS', id: 'HTTPS'},\n {label: 'HTTP Server', id: 'SERVER'},\n {label: 'Indexer scheduler', id: 'SCHEDULER'},\n {label: 'Notifications', id: 'NOTIFICATIONS'},\n {label: 'NZB download status updating', id: 'DOWNLOAD_STATUS_UPDATE'},\n {label: 'Performance', id: 'PERFORMANCE'},\n {label: 'Rejected results', id: 'RESULT_ACCEPTOR'},\n {label: 'Removed trailing words', id: 'TRAILING'},\n {label: 'URL calculation', id: 'URL_CALCULATION'},\n {label: 'User agent mapping', id: 'USER_AGENT'},\n {label: 'VIP expiry', id: 'VIP_EXPIRY'}\n ],\n buttonText: \"None\"\n }\n },\n {\n key: 'historyUserInfoType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'History user info',\n options: [\n {name: 'IP and username', value: 'BOTH'},\n {name: 'IP address', value: 'IP'},\n {name: 'Username', value: 'USERNAME'},\n {name: 'None', value: 'NONE'}\n ],\n help: 'Only affects if value is displayed in the search/download history.',\n hideExpression: '!model.keepHistory'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Backup',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'backupFolder',\n type: 'horizontalInput',\n templateOptions: {\n label: 'Backup folder',\n help: 'Either relative to the NZBHydra data folder or an absolute folder.'\n }\n },\n {\n key: 'backupEveryXDays',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Backup every...',\n addonRight: {\n text: 'days'\n }\n }\n },\n {\n key: 'backupBeforeUpdate',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Backup before update'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Updates'},\n fieldGroup: [\n {\n key: 'updateAutomatically',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Install updates automatically'\n }\n }, {\n key: 'updateToPrereleases',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Install prereleases',\n advanced: true\n }\n },\n {\n key: 'deleteBackupsAfterWeeks',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Delete backups after...',\n addonRight: {\n text: 'weeks'\n },\n advanced: true\n }\n },\n {\n key: 'showUpdateBannerOnDocker',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show update banner when managed externally',\n advanced: true,\n help: 'If enabled a banner will be shown when new versions are available even when NZBHydra is run inside docker or is installed using a package manager (where you wouldn\\'t let NZBHydra update itself).'\n }\n },\n {\n key: 'showWhatsNewBanner',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show info banner after automatic updates',\n help: 'Please keep it enabled, I put some effort into the changelog ;-)',\n advanced: true\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'History',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'keepHistory',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Keep history',\n help: 'Controls search and download history.',\n tooltip: 'If disabled no search or download history will be kept. These sections will be hidden in the GUI. You won\\'t be able to see stats. The database will still contain a short-lived history of transactions that are kept for 24 hours.'\n }\n },\n {\n key: 'keepHistoryForWeeks',\n type: 'horizontalInput',\n hideExpression: '!model.keepHistory',\n templateOptions: {\n type: 'number',\n label: 'Keep history for...',\n addonRight: {\n text: 'weeks'\n },\n min: 1,\n help: 'Only keep history (searches, downloads) for a certain time. Will decrease database size and may improve performance a bit. Rather reduce how long stats are kept.'\n }\n },\n {\n key: 'keepStatsForWeeks',\n type: 'horizontalInput',\n hideExpression: '!model.keepHistory',\n templateOptions: {\n type: 'number',\n label: 'Keep stats for...',\n addonRight: {\n text: 'weeks'\n },\n min: 1,\n help: 'Only keep stats for a certain time. Will decrease database size.'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Database',\n tooltip: 'You should not change these values unless you\\'re either told to or really know what you\\'re doing.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'databaseCompactTime',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Database compact time',\n addonRight: {\n text: 'ms'\n },\n min: 200,\n help: 'The time the database is given to compact (reduce size) when shutting down. Reduce this if shutting down NZBHydra takes too long (database size may increase). Takes effect on next restart.'\n }\n },\n {\n key: 'databaseRetentionTime',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Database retention time',\n addonRight: {\n text: 'ms'\n },\n help: 'How long the db should retain old, persisted data. See here.'\n }\n },\n {\n key: 'databaseWriteDelay',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Database write delay',\n addonRight: {\n text: 'ms'\n },\n help: 'Maximum delay between a commit and flushing the log, in milliseconds. See here.'\n }\n }\n\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {label: 'Other'},\n fieldGroup: [\n {\n key: 'startupBrowser',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Open browser on startup'\n }\n },\n {\n key: 'showNews',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show news',\n help: \"Hydra will occasionally show news when opened. You can always find them in the system section\",\n advanced: true\n }\n },\n {\n key: 'xmx',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'JVM memory',\n addonRight: {\n text: 'MB'\n },\n min: 128,\n help: '256 should suffice except when working with big databases / many indexers. See wiki.',\n advanced: true\n }\n }\n ]\n\n }\n ],\n\n searching: [\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Indexer access',\n tooltip: 'Settings that control how communication with indexers is done and how to handle errors while doing that.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'timeout',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Timeout when accessing indexers',\n help: 'Any web call to an indexer taking longer than this is aborted.',\n min: 1,\n addonRight: {\n text: 'seconds'\n }\n }\n },\n {\n key: 'userAgent',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'User agent',\n help: 'Used when accessing indexers.',\n required: true,\n tooltip: 'Some indexers don\\'t seem to like Hydra and disable access based on the user agent. You can change it here if you want. Please leave it as it is if you have no problems. This allows indexers to gather better statistics on how their API services are used.',\n }\n },\n {\n key: 'userAgents',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Map user agents',\n help: 'Used to map the user agent from accessing services to the service names. Apply words with return key.',\n }\n },\n {\n key: 'ignoreLoadLimitingForInternalSearches',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Ignore load limiting internally',\n help: 'When enabled load limiting defined for indexers will be ignored for internal searches.',\n }\n },\n {\n key: 'ignoreTemporarilyDisabled',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Ignore temporary errors',\n tooltip: \"By default if access to an indexer fails the indexer is disabled for a certain amount of time (for a short while first, then increasingly longer if the problems persist). Disable this and always try these indexers.\",\n }\n }\n ]\n }, {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Category handling',\n tooltip: 'Settings that control the handling of newznab categories (e.g. 2000 for Movies).',\n advanced: true\n },\n fieldGroup: [\n\n {\n key: 'transformNewznabCategories',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Transform newznab categories',\n help: 'Map newznab categories from API searches to configured categories and use all configured newznab categories in searches.'\n }\n },\n {\n key: 'sendTorznabCategories',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Send categories to trackers',\n help: 'If disabled no categories will be included in queries to torznab indexers (trackers).'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Media IDs / Query generation / Query processing',\n tooltip: 'Raw search engines like Binsearch don\\'t support searches based on IDs (e.g. for a movie using an IMDB id). You can enable query generation for these. Hydra will then try to retrieve the movie\\'s or show\\'s title and generate a query, for example \"showname s01e01\". In some cases an ID based search will not provide any results. You can enable a fallback so that in such a case the search will be repeated with a query using the title of the show or movie.'\n },\n fieldGroup: [\n {\n key: 'alwaysConvertIds',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Convert media IDs for...',\n options: [\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'All searches', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"When enabled media ID conversions will always be done even when an indexer supports the already known ID(s).\",\n advanced: true\n }\n },\n {\n key: 'generateQueries',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Generate queries',\n options: [\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'All searches', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"Generate queries for indexers which do not support ID based searches.\"\n }\n },\n {\n key: 'idFallbackToQueryGeneration',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Fallback to generated queries',\n options: [\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'All searches', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"When no results were found for a query ID search again using a generated query (on indexer level).\"\n }\n },\n {\n key: 'language',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'text',\n label: 'Language',\n required: true,\n help: 'Used for movie query generation and autocomplete only.',\n options: [{\"name\": \"Abkhaz\", value: \"ab\"}, {\n \"name\": \"Afar\",\n value: \"aa\"\n }, {\"name\": \"Afrikaans\", value: \"af\"}, {\"name\": \"Akan\", value: \"ak\"}, {\n \"name\": \"Albanian\",\n value: \"sq\"\n }, {\"name\": \"Amharic\", value: \"am\"}, {\n \"name\": \"Arabic\",\n value: \"ar\"\n }, {\"name\": \"Aragonese\", value: \"an\"}, {\"name\": \"Armenian\", value: \"hy\"}, {\n \"name\": \"Assamese\",\n value: \"as\"\n }, {\"name\": \"Avaric\", value: \"av\"}, {\"name\": \"Avestan\", value: \"ae\"}, {\n \"name\": \"Aymara\",\n value: \"ay\"\n }, {\"name\": \"Azerbaijani\", value: \"az\"}, {\n \"name\": \"Bambara\",\n value: \"bm\"\n }, {\"name\": \"Bashkir\", value: \"ba\"}, {\n \"name\": \"Basque\",\n value: \"eu\"\n }, {\"name\": \"Belarusian\", value: \"be\"}, {\"name\": \"Bengali\", value: \"bn\"}, {\n \"name\": \"Bihari\",\n value: \"bh\"\n }, {\"name\": \"Bislama\", value: \"bi\"}, {\n \"name\": \"Bosnian\",\n value: \"bs\"\n }, {\"name\": \"Breton\", value: \"br\"}, {\"name\": \"Bulgarian\", value: \"bg\"}, {\n \"name\": \"Burmese\",\n value: \"my\"\n }, {\"name\": \"Catalan\", value: \"ca\"}, {\n \"name\": \"Chamorro\",\n value: \"ch\"\n }, {\"name\": \"Chechen\", value: \"ce\"}, {\"name\": \"Chichewa\", value: \"ny\"}, {\n \"name\": \"Chinese\",\n value: \"zh\"\n }, {\"name\": \"Chuvash\", value: \"cv\"}, {\n \"name\": \"Cornish\",\n value: \"kw\"\n }, {\"name\": \"Corsican\", value: \"co\"}, {\"name\": \"Cree\", value: \"cr\"}, {\n \"name\": \"Croatian\",\n value: \"hr\"\n }, {\"name\": \"Czech\", value: \"cs\"}, {\"name\": \"Danish\", value: \"da\"}, {\n \"name\": \"Divehi\",\n value: \"dv\"\n }, {\"name\": \"Dutch\", value: \"nl\"}, {\n \"name\": \"Dzongkha\",\n value: \"dz\"\n }, {\"name\": \"English\", value: \"en\"}, {\n \"name\": \"Esperanto\",\n value: \"eo\"\n }, {\"name\": \"Estonian\", value: \"et\"}, {\"name\": \"Ewe\", value: \"ee\"}, {\n \"name\": \"Faroese\",\n value: \"fo\"\n }, {\"name\": \"Fijian\", value: \"fj\"}, {\"name\": \"Finnish\", value: \"fi\"}, {\n \"name\": \"French\",\n value: \"fr\"\n }, {\"name\": \"Fula\", value: \"ff\"}, {\n \"name\": \"Galician\",\n value: \"gl\"\n }, {\"name\": \"Georgian\", value: \"ka\"}, {\"name\": \"German\", value: \"de\"}, {\n \"name\": \"Greek\",\n value: \"el\"\n }, {\"name\": \"Guaraní\", value: \"gn\"}, {\n \"name\": \"Gujarati\",\n value: \"gu\"\n }, {\"name\": \"Haitian\", value: \"ht\"}, {\"name\": \"Hausa\", value: \"ha\"}, {\n \"name\": \"Hebrew\",\n value: \"he\"\n }, {\"name\": \"Herero\", value: \"hz\"}, {\n \"name\": \"Hindi\",\n value: \"hi\"\n }, {\"name\": \"Hiri Motu\", value: \"ho\"}, {\n \"name\": \"Hungarian\",\n value: \"hu\"\n }, {\"name\": \"Interlingua\", value: \"ia\"}, {\n \"name\": \"Indonesian\",\n value: \"id\"\n }, {\"name\": \"Interlingue\", value: \"ie\"}, {\n \"name\": \"Irish\",\n value: \"ga\"\n }, {\"name\": \"Igbo\", value: \"ig\"}, {\"name\": \"Inupiaq\", value: \"ik\"}, {\n \"name\": \"Ido\",\n value: \"io\"\n }, {\"name\": \"Icelandic\", value: \"is\"}, {\n \"name\": \"Italian\",\n value: \"it\"\n }, {\"name\": \"Inuktitut\", value: \"iu\"}, {\"name\": \"Japanese\", value: \"ja\"}, {\n \"name\": \"Javanese\",\n value: \"jv\"\n }, {\"name\": \"Kalaallisut\", value: \"kl\"}, {\n \"name\": \"Kannada\",\n value: \"kn\"\n }, {\"name\": \"Kanuri\", value: \"kr\"}, {\"name\": \"Kashmiri\", value: \"ks\"}, {\n \"name\": \"Kazakh\",\n value: \"kk\"\n }, {\"name\": \"Khmer\", value: \"km\"}, {\n \"name\": \"Kikuyu\",\n value: \"ki\"\n }, {\"name\": \"Kinyarwanda\", value: \"rw\"}, {\"name\": \"Kyrgyz\", value: \"ky\"}, {\n \"name\": \"Komi\",\n value: \"kv\"\n }, {\"name\": \"Kongo\", value: \"kg\"}, {\"name\": \"Korean\", value: \"ko\"}, {\n \"name\": \"Kurdish\",\n value: \"ku\"\n }, {\"name\": \"Kwanyama\", value: \"kj\"}, {\n \"name\": \"Latin\",\n value: \"la\"\n }, {\"name\": \"Luxembourgish\", value: \"lb\"}, {\n \"name\": \"Ganda\",\n value: \"lg\"\n }, {\"name\": \"Limburgish\", value: \"li\"}, {\"name\": \"Lingala\", value: \"ln\"}, {\n \"name\": \"Lao\",\n value: \"lo\"\n }, {\"name\": \"Lithuanian\", value: \"lt\"}, {\n \"name\": \"Luba-Katanga\",\n value: \"lu\"\n }, {\"name\": \"Latvian\", value: \"lv\"}, {\"name\": \"Manx\", value: \"gv\"}, {\n \"name\": \"Macedonian\",\n value: \"mk\"\n }, {\"name\": \"Malagasy\", value: \"mg\"}, {\n \"name\": \"Malay\",\n value: \"ms\"\n }, {\"name\": \"Malayalam\", value: \"ml\"}, {\"name\": \"Maltese\", value: \"mt\"}, {\n \"name\": \"Māori\",\n value: \"mi\"\n }, {\"name\": \"Marathi\", value: \"mr\"}, {\n \"name\": \"Marshallese\",\n value: \"mh\"\n }, {\"name\": \"Mongolian\", value: \"mn\"}, {\"name\": \"Nauru\", value: \"na\"}, {\n \"name\": \"Navajo\",\n value: \"nv\"\n }, {\"name\": \"Northern Ndebele\", value: \"nd\"}, {\n \"name\": \"Nepali\",\n value: \"ne\"\n }, {\"name\": \"Ndonga\", value: \"ng\"}, {\n \"name\": \"Norwegian Bokmål\",\n value: \"nb\"\n }, {\"name\": \"Norwegian Nynorsk\", value: \"nn\"}, {\n \"name\": \"Norwegian\",\n value: \"no\"\n }, {\"name\": \"Nuosu\", value: \"ii\"}, {\n \"name\": \"Southern Ndebele\",\n value: \"nr\"\n }, {\"name\": \"Occitan\", value: \"oc\"}, {\n \"name\": \"Ojibwe\",\n value: \"oj\"\n }, {\"name\": \"Old Church Slavonic\", value: \"cu\"}, {\"name\": \"Oromo\", value: \"om\"}, {\n \"name\": \"Oriya\",\n value: \"or\"\n }, {\"name\": \"Ossetian\", value: \"os\"}, {\"name\": \"Panjabi\", value: \"pa\"}, {\n \"name\": \"Pāli\",\n value: \"pi\"\n }, {\"name\": \"Persian\", value: \"fa\"}, {\n \"name\": \"Polish\",\n value: \"pl\"\n }, {\"name\": \"Pashto\", value: \"ps\"}, {\n \"name\": \"Portuguese\",\n value: \"pt\"\n }, {\"name\": \"Quechua\", value: \"qu\"}, {\"name\": \"Romansh\", value: \"rm\"}, {\n \"name\": \"Kirundi\",\n value: \"rn\"\n }, {\"name\": \"Romanian\", value: \"ro\"}, {\n \"name\": \"Russian\",\n value: \"ru\"\n }, {\"name\": \"Sanskrit\", value: \"sa\"}, {\"name\": \"Sardinian\", value: \"sc\"}, {\n \"name\": \"Sindhi\",\n value: \"sd\"\n }, {\"name\": \"Northern Sami\", value: \"se\"}, {\n \"name\": \"Samoan\",\n value: \"sm\"\n }, {\"name\": \"Sango\", value: \"sg\"}, {\"name\": \"Serbian\", value: \"sr\"}, {\n \"name\": \"Gaelic\",\n value: \"gd\"\n }, {\"name\": \"Shona\", value: \"sn\"}, {\"name\": \"Sinhala\", value: \"si\"}, {\n \"name\": \"Slovak\",\n value: \"sk\"\n }, {\"name\": \"Slovene\", value: \"sl\"}, {\n \"name\": \"Somali\",\n value: \"so\"\n }, {\"name\": \"Southern Sotho\", value: \"st\"}, {\n \"name\": \"Spanish\",\n value: \"es\"\n }, {\"name\": \"Sundanese\", value: \"su\"}, {\"name\": \"Swahili\", value: \"sw\"}, {\n \"name\": \"Swati\",\n value: \"ss\"\n }, {\"name\": \"Swedish\", value: \"sv\"}, {\"name\": \"Tamil\", value: \"ta\"}, {\n \"name\": \"Telugu\",\n value: \"te\"\n }, {\"name\": \"Tajik\", value: \"tg\"}, {\n \"name\": \"Thai\",\n value: \"th\"\n }, {\"name\": \"Tigrinya\", value: \"ti\"}, {\n \"name\": \"Tibetan Standard\",\n value: \"bo\"\n }, {\"name\": \"Turkmen\", value: \"tk\"}, {\"name\": \"Tagalog\", value: \"tl\"}, {\n \"name\": \"Tswana\",\n value: \"tn\"\n }, {\"name\": \"Tonga\", value: \"to\"}, {\"name\": \"Turkish\", value: \"tr\"}, {\n \"name\": \"Tsonga\",\n value: \"ts\"\n }, {\"name\": \"Tatar\", value: \"tt\"}, {\n \"name\": \"Twi\",\n value: \"tw\"\n }, {\"name\": \"Tahitian\", value: \"ty\"}, {\n \"name\": \"Uyghur\",\n value: \"ug\"\n }, {\"name\": \"Ukrainian\", value: \"uk\"}, {\"name\": \"Urdu\", value: \"ur\"}, {\n \"name\": \"Uzbek\",\n value: \"uz\"\n }, {\"name\": \"Venda\", value: \"ve\"}, {\n \"name\": \"Vietnamese\",\n value: \"vi\"\n }, {\"name\": \"Volapük\", value: \"vo\"}, {\"name\": \"Walloon\", value: \"wa\"}, {\n \"name\": \"Welsh\",\n value: \"cy\"\n }, {\"name\": \"Wolof\", value: \"wo\"}, {\n \"name\": \"Western Frisian\",\n value: \"fy\"\n }, {\"name\": \"Xhosa\", value: \"xh\"}, {\"name\": \"Yiddish\", value: \"yi\"}, {\n \"name\": \"Yoruba\",\n value: \"yo\"\n }, {\"name\": \"Zhuang\", value: \"za\"}, {\"name\": \"Zulu\", value: \"zu\"}]\n }\n },\n {\n key: 'replaceUmlauts',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Replace umlauts',\n help: 'Replace german umlauts and special characters (ä, ö, ü and ß) in external request queries.'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Result filters',\n tooltip: 'This section allows you to define global filters which will be applied to all search results. You can define words and regexes which must or must not be matched for a search result to be matched. You can also exclude certain usenet posters and groups which are known for spamming. You can define forbidden and required words for categories in the next tab (Categories). Usually required or forbidden words are applied on a word base, so they must form a complete word in a title. Only if they contain a dash or a dot they may appear anywhere in the title. Example: \"ea\" matches \"something.from.ea\" but not \"release.from.other\". \"web-dl\" matches \"title.web-dl\" and \"someweb-dl\".'\n },\n fieldGroup: [\n {\n key: 'applyRestrictions',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Apply word filters',\n options: [\n {name: 'All searches', value: 'BOTH'},\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"For which type of search word/regex filters will be applied\"\n }\n },\n {\n key: 'forbiddenWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden words',\n help: \"Results with any of these words in the title will be ignored. Title is converted to lowercase before. Apply words with return key.\",\n tooltip: 'One forbidden word in a result title dismisses the result.'\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'forbiddenRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Forbidden regex',\n help: 'Must not be present in a title (case is ignored).',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'requiredWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Required words',\n help: \"Only results with titles that contain *all* words will be used. Title is converted to lowercase before. Apply words with return key.\",\n tooltip: 'If any of the required words is not found anywhere in a result title it\\'s also dismissed.'\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'requiredRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Required regex',\n help: 'Must be present in a title (case is ignored).',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n\n {\n key: 'forbiddenGroups',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden groups',\n help: 'Posts from any groups containing any of these words will be ignored. Apply words with return key.',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.searching.applyRestrictions === \"NONE\";\n }\n },\n {\n key: 'forbiddenPosters',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden posters',\n help: 'Posts from any posters containing any of these words will be ignored. Apply words with return key.',\n advanced: true\n }\n },\n {\n key: 'languagesToKeep',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Languages to keep',\n help: 'If an indexer returns the language in the results only those results with configured languages will be used. Apply words with return key.'\n }\n },\n {\n key: 'maxAge',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Maximum results age',\n help: 'Results older than this are ignored. Can be overwritten per search. Apply words with return key.',\n addonRight: {\n text: 'days'\n }\n }\n },\n {\n key: 'minSeeders',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Minimum # seeders',\n help: 'Torznab results with fewer seeders will be ignored.'\n }\n },\n {\n key: 'ignorePassworded',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Ignore passworded releases',\n help: \"Not all indexers provide this information\",\n tooltip: 'Some indexers provide information if a release is passworded. If you select to ignore these releases only those will be ignored of which I know for sure that they\\'re actually passworded.'\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Result processing'\n },\n fieldGroup: [\n {\n key: 'wrapApiErrors',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'text',\n label: 'Wrap API errors in empty results page',\n help: 'When enabled accessing tools will think the search was completed successfully but without results.',\n tooltip: 'In (hopefully) rare cases Hydra may crash when processing an API search request. You can enable to return an empty search page in these cases (if Hydra hasn\\'t crashed altogether ). This means that the calling tool (e.g. Sonarr) will think that the indexer (Hydra) is fine but just didn\\'t return a result. That way Hydra won\\'t be disabled as indexer but on the downside you may not be directly notified that an error occurred.',\n advanced: true\n }\n },\n {\n key: 'removeTrailing',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Remove trailing...',\n help: 'Removed from title if it ends with either of these. Case insensitive and disregards leading/trailing spaces. Allows wildcards (\"*\"). Apply words with return key.',\n tooltip: 'Hydra contains a predefined list of words which will be removed if a search result title ends with them. This allows better duplicate detection and cleans up the titles. Trailing words will be removed until none of the defined strings are found at the end of the result title.'\n }\n },\n {\n key: 'useOriginalCategories',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Use original categories',\n help: 'Enable to use the category descriptions provided by the indexer.',\n tooltip: 'Hydra attempts to parse the provided newznab category IDs for results and map them to the configured categories. In some cases this may lead to category names which are not quite correct. You can select to use the original category name used by the indexer. This will only affect which category name is shown in the results.',\n advanced: true\n }\n }\n ]\n },\n {\n type: 'repeatSection',\n key: 'customMappings',\n model: rootModel.searching,\n templateOptions: {\n tooltip: 'Here you can define mappings to modify either queries or titles for search requests or to dynamically change the titles of found results. The former allows you, for example, to change requests made by external tools, the latter to clean up results by indexers in a more advanced way.',\n btnText: 'Add new custom mapping',\n altLegendText: 'Mapping',\n headline: 'Custom mappings of queries, search titles and result titles',\n advanced: true,\n fields: [\n {\n key: 'affectedValue',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Affected value',\n options: [\n {name: 'Query', value: 'QUERY'},\n {name: 'Search title', value: 'TITLE'},\n {name: 'Result title', value: 'RESULT_TITLE'},\n ],\n required: true,\n help: \"Determines which value of the search request or result will be processed\"\n }\n },\n {\n key: 'searchType',\n type: 'horizontalSelect',\n hideExpression: 'model.affectedValue === \"RESULT_TITLE\"',\n templateOptions: {\n label: 'Search type',\n options: [\n {name: 'General', value: 'SEARCH'},\n {name: 'Audio', value: 'MUSIC'},\n {name: 'EBook', value: 'BOOK'},\n {name: 'Movie', value: 'MOVIE'},\n {name: 'TV', value: 'TVSEARCH'}\n ],\n help: \"Determines in what context the mapping will be executed\"\n }\n },\n {\n key: 'from',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Input pattern',\n help: 'Pattern which must match the query or title of a search request. You may use regexes in groups which can be referenced in the output puttern by using {group:regex}. Case insensitive.',\n required: true\n }\n },\n {\n key: 'to',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Output pattern',\n help: 'If a query or title matches the input pattern it will be replaced using this. You may reference groups from the input pattern by using {group}. Additionally you may use {season:0} or {season:00} or {episode:0} or {episode:00} (with and without leading zeroes).',\n required: true\n }\n },\n {\n type: 'customMappingTest',\n }\n ],\n defaultModel: {\n searchType: null,\n affectedValue: null,\n from: null,\n to: null\n }\n }\n },\n\n\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Result display'\n },\n fieldGroup: [\n {\n key: 'loadAllCachedOnInternal',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Display all retrieved results',\n help: 'Load all results already retrieved from indexers. Might make sorting / filtering a bit slower. Will still be paged according to the limit set above.',\n advanced: true\n }\n },\n {\n key: 'loadLimitInternal',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Display...',\n addonRight: {\n text: 'results per page'\n },\n max: 500,\n required: true,\n help: 'Determines the number of results shown on one page. This might also cause more API hits because indexers are queried until the number of results is matched or all indexers are exhausted. Limit is 500.',\n advanced: true\n }\n },\n {\n key: 'coverSize',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Cover width',\n addonRight: {\n text: 'px'\n },\n required: true,\n help: 'Determines width of covers in search results (when enabled in display options).'\n }\n }\n ]\n }, {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Quick filters'\n },\n fieldGroup: [\n {\n key: 'showQuickFilterButtons',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show quick filters',\n help: 'Show quick filter buttons for movie and TV results.'\n }\n },\n {\n key: 'alwaysShowQuickFilterButtons',\n type: 'horizontalSwitch',\n hideExpression: '!model.showQuickFilterButtons',\n templateOptions: {\n type: 'switch',\n label: 'Always show quick filters',\n help: 'Show all quick filter buttons for all types of searches.',\n advanced: true\n }\n },\n {\n key: 'customQuickFilterButtons',\n type: 'horizontalChips',\n hideExpression: '!model.showQuickFilterButtons',\n templateOptions: {\n type: 'text',\n label: 'Custom quick filters',\n help: 'Enter in the format \"DisplayName=Required1,Required2\". Apply values with enter key.',\n tooltip: 'E.g. use \"WEB=webdl,web-dl.\" for a quick filter with the name \"WEB\" to be displayed that searches for \"webdl\" and \"web-dl\" in lowercase search results.',\n advanced: true\n }\n\n },\n {\n key: 'preselectQuickFilterButtons',\n type: 'horizontalMultiselect',\n hideExpression: '!model.showQuickFilterButtons',\n templateOptions: {\n label: 'Preselect quickfilters',\n help: 'Choose which quickfilters will be selected by default.',\n options: [\n {id: 'source|camts', label: 'CAM / TS'},\n {id: 'source|tv', label: 'TV'},\n {id: 'source|web', label: 'WEB'},\n {id: 'source|dvd', label: 'DVD'},\n {id: 'source|bluray', label: 'Blu-Ray'},\n {id: 'quality|q480p', label: '480p'},\n {id: 'quality|q720p', label: '720p'},\n {id: 'quality|q1080p', label: '1080p'},\n {id: 'quality|q2160p', label: '2160p'},\n {id: 'other|q3d', label: '3D'},\n {id: 'other|qx265', label: 'x265'},\n {id: 'other|qhevc', label: 'HEVC'},\n ],\n optionsFunction: function (model) {\n var customQuickFilters = [];\n _.each(model.customQuickFilterButtons, function (entry) {\n var split1 = entry.split(\"=\");\n var displayName = split1[0];\n customQuickFilters.push({id: \"custom|\" + displayName, label: displayName})\n })\n return customQuickFilters;\n },\n tooltip: 'To select custom quickfilters you just entered please save the config first.',\n buttonText: \"None\",\n advanced: true\n }\n }\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Duplicate detection',\n tooltip: 'Hydra tries to find duplicate results from different indexers using heuristics. You can control the parameters for that but usually the default values work quite well.',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'duplicateSizeThresholdInPercent',\n type: 'horizontalPercentInput',\n templateOptions: {\n type: 'text',\n label: 'Duplicate size threshold',\n required: true,\n addonRight: {\n text: '%'\n }\n\n }\n },\n {\n key: 'duplicateAgeThreshold',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Duplicate age threshold',\n required: true,\n addonRight: {\n text: 'hours'\n }\n }\n }\n\n ]\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Other',\n advanced: true\n },\n fieldGroup: [\n {\n key: 'keepSearchResultsForDays',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Store results for ...',\n addonRight: {\n text: 'days'\n },\n required: true,\n tooltip: 'Found results are stored in the database for this long until they\\'re deleted. After that any links to Hydra results still stored elsewhere become invalid. You can increase the limit if you want, the disc space needed is negligible (about 75 MB for 7 days on my server).'\n }\n },\n {\n key: 'globalCacheTimeMinutes',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Results cache time',\n help: 'When set search results will be cached for this time. Any search with the same parameters will return the cached results. API cache time parameters will be preferred. See wiki.',\n addonRight: {\n text: 'minutes'\n }\n }\n }\n ]\n }\n ],\n\n categoriesConfig: [\n {\n key: 'enableCategorySizes',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Category sizes',\n help: \"Preset min and max sizes depending on the selected category\",\n tooltip: 'Preset range of minimum and maximum sizes for its categories. When you select a category in the search area the appropriate fields are filled with these values.'\n }\n },\n {\n key: 'defaultCategory',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Default category',\n options: [],\n help: \"Set a default category. Reload page to set a category you just added.\"\n },\n controller: function ($scope) {\n var options = [];\n options.push({name: 'All', value: 'All'});\n _.each($scope.model.categories, function (cat) {\n options.push({name: cat.name, value: cat.name});\n });\n $scope.to.options = options;\n }\n },\n {\n type: 'help',\n templateOptions: {\n type: 'help',\n lines: [\n \"The category configuration is not validated in any way. You can seriously fuck up Hydra's results and overall behavior so take care.\",\n \"Restrictions will taken from a result's category, not the search request category which may not always be the same.\"\n ],\n marginTop: '50px',\n advanced: true\n }\n },\n {\n type: 'repeatSection',\n key: 'categories',\n model: rootModel.categoriesConfig,\n templateOptions: {\n btnText: 'Add new category',\n headline: 'Categories',\n advanced: true,\n fields: [\n {\n key: 'name',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Name',\n help: 'Renaming categories might cause problems with repeating searches from the history.',\n required: true\n }\n },\n {\n key: 'searchType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Search type',\n options: [\n {name: 'General', value: 'SEARCH'},\n {name: 'Audio', value: 'MUSIC'},\n {name: 'EBook', value: 'BOOK'},\n {name: 'Movie', value: 'MOVIE'},\n {name: 'TV', value: 'TVSEARCH'}\n ],\n help: \"Determines how indexers will be searched and if autocompletion is available in the GUI\"\n }\n },\n {\n key: 'subtype',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Sub type',\n options: [\n {name: 'Anime', value: 'ANIME'},\n {name: 'Audiobook', value: 'AUDIOBOOK'},\n {name: 'Comic', value: 'COMIC'},\n {name: 'Ebook', value: 'EBOOK'},\n {name: 'None', value: 'NONE'}\n ],\n help: \"Special search type. Used for indexer specific mappings between categories and newznab IDs\"\n }\n },\n {\n key: 'applyRestrictionsType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Apply restrictions',\n options: [\n {name: 'All searches', value: 'BOTH'},\n {name: 'Internal searches', value: 'INTERNAL'},\n {name: 'API searches', value: 'API'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"For which type of search word restrictions will be applied\"\n }\n },\n {\n key: 'requiredWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Required words',\n help: \"Must *all* be present in a title which is converted to lowercase before. Apply words with return key.\"\n }\n },\n {\n key: 'requiredRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Required regex',\n help: 'Must be present in a title (case is ignored).'\n }\n },\n {\n key: 'forbiddenWords',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Forbidden words',\n help: \"None may be present in a title which is converted to lowercase before. Apply words with return key.\"\n }\n },\n {\n key: 'forbiddenRegex',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Forbidden regex',\n help: 'Must not be present in a title (case is ignored).'\n }\n },\n {\n wrapper: 'settingWrapper',\n templateOptions: {\n label: 'Size preset',\n help: \"Will set these values on the search page\"\n },\n fieldGroup: [\n {\n key: 'minSizePreset',\n type: 'duoSetting',\n templateOptions: {\n addonRight: {\n text: 'MB'\n }\n\n }\n },\n {\n type: 'duolabel'\n },\n {\n key: 'maxSizePreset',\n type: 'duoSetting', templateOptions: {addonRight: {text: 'MB'}}\n }\n ]\n },\n {\n key: 'applySizeLimitsToApi',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Limit API results size',\n help: \"Enable to apply the size preset to API results from this category\"\n }\n },\n {\n key: 'newznabCategories',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Newznab categories',\n help: 'Map newznab categories to Hydra categories. Used for parsing and when searching internally. Apply categories with return key.',\n tooltip: 'Hydra tries to map API search (newnzab) categories to its internal list of categories, going from specific to general. Example: If an API search is done with a catagory that matches those of \"Movies HD\" the settings for that category are used. Otherwise it checks if it matches the \"Movies\" category and, if yes, uses that one. If that one doesn\\'t match no category settings are used.

          ' +\n 'Related to that you must also define the newznab categories for every Hydra category, e.g. decide if the category for foreign movies (2010) is used for movie searches. This also controls the category mapping described above. You may combine newznab categories using \"&\" to require multiple numbers to be present in a result. For example \"2010&11000\" would require a search result to contain both 2010 and 11000 for that category to match.

          ' +\n 'Note: When an API search defines categories the internal mapping is only used for the forbidden and required words. The search requests to your newznab indexers will still use the categories from the original request, not the ones configured here.'\n }\n },\n {\n key: 'ignoreResultsFrom',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Ignore results',\n options: [\n {name: 'For all searches', value: 'BOTH'},\n {name: 'For internal searches', value: 'INTERNAL'},\n {name: 'For API searches', value: 'API'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"Ignore results from this category\",\n tooltip: 'If you want you can entirely ignore results from categories. Results from these categories will not show in the searches. If you select \"Internal\" or \"Always\" this category will also not be selectable on the search page.'\n }\n }\n\n ],\n defaultModel: {\n name: null,\n applySizeLimitsToApi: false,\n applyRestrictionsType: \"NONE\",\n forbiddenRegex: null,\n forbiddenWords: [],\n ignoreResultsFrom: \"NONE\",\n mayBeSelected: true,\n maxSizePreset: null,\n minSizePreset: null,\n newznabCategories: [],\n preselect: true,\n requiredRegex: null,\n requiredWords: [],\n searchType: \"SEARCH\",\n subtype: \"NONE\"\n }\n }\n }\n ],\n downloading: [\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'General',\n tooltip: 'Hydra allows sending NZB search results directly to downloaders (NZBGet, sabnzbd). Torrent downloaders are not supported.'\n },\n fieldGroup: [\n {\n key: 'saveTorrentsTo',\n type: 'fileInput',\n templateOptions: {\n label: 'Torrent black hole',\n help: 'Allow torrents to be saved in this folder from the search results. Ignored if not set.',\n type: \"folder\"\n }\n },\n {\n key: 'saveNzbsTo',\n type: 'fileInput',\n templateOptions: {\n label: 'NZB black hole',\n help: 'Allow NZBs to be saved in this folder from the search results. Ignored if not set.',\n type: \"folder\"\n }\n },\n {\n key: 'nzbAccessType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'NZB access type',\n options: [\n {name: 'Proxy NZBs from indexer', value: 'PROXY'},\n {name: 'Redirect to the indexer', value: 'REDIRECT'}\n ],\n help: \"How access to NZBs is provided when NZBs are downloaded (by the user or external tools). Proxying is recommended as it allows fallback for failed downloads (see below)..\",\n tooltip: 'NZB downloads from Hydra can either be achieved by redirecting the requester to the original indexer or by downloading the NZB from the indexer and serving this. Redirecting has the advantage that it causes the least load on Hydra but also the disadvantage that the requester might be forwarded to an indexer link that contains the indexer\\'s API key. To prevent that select to proxy NZBs. It also allows fallback for failed downloads (next option).',\n advanced: true\n\n }\n },\n {\n key: 'externalUrl',\n type: 'horizontalInput',\n hideExpression: function ($viewValue, $modelValue, scope) {\n return !_.any(scope.model.downloaders, function (downloader) {\n return downloader.nzbAddingType === \"SEND_LINK\";\n });\n },\n templateOptions: {\n label: 'External URL',\n help: 'Used for links when sending links to the downloader.',\n tooltip: 'When using \"Add links\" to add NZBs to your downloader the links are usually calculated using the URL with which you accessed NZBHydra. This might be a URL that\\'s not accessible by the downloader (e.g. when it\\'s inside a docker container). Set the URL for NZBHydra that\\'s accessible by the downloader here and it will be used instead. ',\n advanced: true\n }\n },\n\n {\n key: 'fallbackForFailed',\n type: 'horizontalSelect',\n hideExpression: 'model.nzbAccessType === \"REDIRECT\"',\n templateOptions: {\n label: 'Fallback for failed downloads',\n options: [\n {name: 'GUI downloads', value: 'INTERNAL'},\n {name: 'API downloads', value: 'API'},\n {name: 'All downloads', value: 'BOTH'},\n {name: 'Never', value: 'NONE'}\n ],\n help: \"Fallback to similar results when a download fails. Only available when proxying NZBs (see above).\",\n tooltip: \"When you or an external program tries to download an NZB from NZBHydra the download may fail because the indexer is offline or its download limit has been reached. You can use this setting for NZBHydra to try and fall back on results from other indexers. It will search for results with the same name that were the result from the same search as where the download originated from. It will *not* execute another search.\"\n }\n },\n {\n key: 'sendMagnetLinks',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Send magnet links',\n help: \"Enable to send magnet links to the associated program on the server machine. Won't work with docker\"\n }\n },\n {\n key: 'updateStatuses',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Update statuses',\n help: \"Query your downloader for status updates of downloads\",\n advanced: true\n }\n },\n {\n key: 'showDownloaderStatus',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Show downloader footer',\n help: \"Show footer with downloader status\",\n advanced: true\n }\n },\n {\n key: 'primaryDownloader',\n type: 'horizontalSelect',\n hideExpression: 'model.downloaders.length <= 1 || !model.showDownloaderStatus',\n templateOptions: {\n label: 'Primary downloader',\n options: [],\n help: \"This downloader's state will be shown in the footer.\",\n tooltip: \"To select a downloader you just added please save the config first.\",\n optionsFunction: function (model) {\n var downloaders = [];\n _.each(model.downloaders, function (downloader) {\n downloaders.push({name: downloader.name, value: downloader.name})\n })\n return downloaders;\n },\n optionsFunctionAfter: function (model) {\n if (!model.primaryDownloader) {\n model.primaryDownloader = model.downloaders[0].name;\n }\n }\n }\n },\n ]\n },\n {\n wrapper: 'fieldset',\n key: 'downloaders',\n templateOptions: {label: 'Downloaders'},\n fieldGroup: [\n {\n type: \"downloaderConfig\",\n data: {}\n }\n ]\n }\n ],\n\n indexers: [\n {\n type: \"indexers\",\n data: {}\n },\n {\n type: 'recheckAllCaps'\n }\n ],\n auth: [\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Main',\n\n },\n fieldGroup: [\n {\n key: 'authType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Auth type',\n options: [\n {name: 'None', value: 'NONE'},\n {name: 'HTTP Basic auth', value: 'BASIC'},\n {name: 'Login form', value: 'FORM'}\n ],\n tooltip: '
            ' +\n '
          • With auth type \"None\" all areas are unrestricted.
          • ' +\n '
          • With auth type \"Form\" the basic page is loaded and login is done via a form.
          • ' +\n '
          • With auth type \"Basic\" you login via basic HTTP authentication. With all areas restricted this is the most secure as nearly no data is loaded from the server before you auth. Logging out is not supported with basic auth.
          • ' +\n '
          '\n }\n },\n {\n key: 'authHeader',\n type: 'horizontalInput',\n templateOptions: {\n type: 'string',\n label: 'Auth header',\n help: 'Name of header that provides the username in requests from secure sources.',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n }\n },\n {\n key: 'authHeaderIpRanges',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Secure IP ranges',\n help: 'IP ranges from which the auth header will be accepted. Apply with return key. Use values like \"192.168.0.1-192.168.0.100\" or single IP addresses like \"127.0.0.1\".',\n advanced: true\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\" || _.isNullOrEmpty(rootModel.auth.authHeader);\n }\n },\n {\n key: 'rememberUsers',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Remember users',\n help: 'Remember users with cookie for 14 days.'\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n }\n },\n {\n key: 'rememberMeValidityDays',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Cookie expiry',\n help: 'How long users are remembered.',\n addonRight: {\n text: 'days'\n },\n advanced: true\n }\n }\n\n ]\n },\n\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Restrictions',\n tooltip: 'Select which areas/features can only be accessed by logged in users (i.e. are restricted). If you don\\'t to allow anonymous users to do anything just leave everything selected.
          You can decide for every user if he is allowed to:
          ' +\n '
            \\n' +\n '
          • view the search page at all
          • \\n' +\n '
          • view the stats
          • \\n' +\n '
          • access the admin area (config and control)
          • \\n' +\n '
          • view links for downloading NZBs and see their details
          • \\n' +\n '
          • may select which indexers are used for search.
          • \\n' +\n '
          '\n },\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n },\n fieldGroup: [\n {\n key: 'restrictSearch',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict searching',\n help: 'Restrict access to searching.'\n }\n },\n {\n key: 'restrictStats',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict stats',\n help: 'Restrict access to stats.'\n }\n },\n {\n key: 'restrictAdmin',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict admin',\n help: 'Restrict access to admin functions.'\n }\n },\n {\n key: 'restrictDetailsDl',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict NZB details & DL',\n help: 'Restrict NZB details, comments and download links.'\n }\n },\n {\n key: 'restrictIndexerSelection',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Restrict indexer selection box',\n help: 'Restrict visibility of indexer selection box in search. Affects only GUI.'\n }\n },\n {\n key: 'allowApiStats',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Allow stats access',\n help: 'Allow access to stats via external API.'\n }\n }\n ]\n },\n\n {\n type: 'repeatSection',\n key: 'users',\n model: rootModel.auth,\n hideExpression: function () {\n return rootModel.auth.authType === \"NONE\";\n },\n templateOptions: {\n btnText: 'Add new user',\n altLegendText: 'Authless',\n headline: 'Users',\n fields: [\n {\n key: 'username',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Username',\n required: true\n }\n },\n {\n key: 'password',\n type: 'passwordSwitch',\n templateOptions: {\n type: 'password',\n label: 'Password',\n required: true\n }\n },\n {\n key: 'maySeeAdmin',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see admin area'\n }\n },\n {\n key: 'maySeeStats',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see stats'\n },\n hideExpression: 'model.maySeeAdmin'\n },\n {\n key: 'maySeeDetailsDl',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see NZB details & DL links'\n },\n hideExpression: 'model.maySeeAdmin'\n },\n {\n key: 'showIndexerSelection',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'May see indexer selection box'\n },\n hideExpression: 'model.maySeeAdmin'\n }\n ],\n defaultModel: {\n username: null,\n password: null,\n token: null,\n maySeeStats: true,\n maySeeAdmin: true,\n maySeeDetailsDl: true,\n showIndexerSelection: true\n }\n }\n }\n ],\n notificationConfig: [\n {\n type: 'help',\n templateOptions: {\n type: 'help',\n lines: [\n \"NZBHydra supports sending and displaying notifications for certain events. You can enable notifications for each event by adding entries below.\",\n 'NZBHydra uses Apprise to communicate with the actual notification providers. You need either a) an instance of Apprise API running or b) an Apprise runnable accessible by NZBHydra. Either are not part of NZBHydra.',\n \"NZBHydra will also show notifications on the GUI if enabled.\"\n ]\n }\n },\n {\n wrapper: 'fieldset',\n templateOptions: {\n label: 'Main'\n },\n fieldGroup: [\n\n {\n key: 'appriseType',\n type: 'horizontalSelect',\n templateOptions: {\n type: 'select',\n label: 'Apprise type',\n options: [\n {name: 'None', value: 'NONE'},\n {name: 'API', value: 'API'},\n {name: 'CLI', value: 'CLI'}\n ]\n }\n },\n {\n key: 'appriseApiUrl',\n type: 'horizontalInput',\n templateOptions: {\n type: 'string',\n label: 'Apprise API URL',\n help: 'URL of Apprise API to send notifications to.'\n },\n hideExpression: 'model.appriseType !== \"API\"'\n },\n {\n key: 'appriseCliPath',\n type: 'fileInput',\n templateOptions: {\n type: 'file',\n label: 'Apprise runnable',\n help: 'Full path of of Apprise runnable to execute.'\n },\n hideExpression: 'model.appriseType !== \"CLI\"'\n },\n {\n key: 'displayNotifications',\n type: 'horizontalSwitch',\n templateOptions: {\n type: 'switch',\n label: 'Display notifications',\n help: 'If enabled notifications will be shown on the GUI.'\n }\n },\n {\n key: 'displayNotificationsMax',\n type: 'horizontalInput',\n templateOptions: {\n type: 'number',\n label: 'Show max notifications',\n help: 'Max number of notifications to show on the GUI. If more have piled up a notification will indicate this and link to the notification history.'\n },\n hideExpression: '!model.displayNotifications'\n },\n {\n key: 'filterOuts',\n type: 'horizontalChips',\n templateOptions: {\n type: 'text',\n label: 'Hide if message contains...',\n help: 'Apply values with return key. Surround with \"/\" for regex (e.g. /contains[0-9]This/). Case insensitive.',\n\n },\n hideExpression: '!model.displayNotifications'\n }\n ]\n },\n\n {\n type: 'notificationSection',\n key: 'entries',\n model: rootModel.notificationConfig,\n templateOptions: {\n btnText: 'Add new notification',\n altLegendText: 'Notification',\n headline: 'Notifications',\n fields: [\n {\n key: 'appriseUrls',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'URLs',\n help: 'One or more URLs identifying where the notification should be sent to, comma-separated.'\n }\n },\n {\n key: 'titleTemplate',\n type: 'horizontalInput',\n templateOptions: {\n type: 'text',\n label: 'Title template'\n },\n controller: notificationTemplateHelpController\n },\n {\n key: 'bodyTemplate',\n type: 'horizontalTextArea',\n templateOptions: {\n type: 'text',\n label: 'Body template',\n required: true\n },\n controller: notificationTemplateHelpController\n },\n {\n key: 'messageType',\n type: 'horizontalSelect',\n templateOptions: {\n label: 'Message type',\n options: [\n {name: 'Info', value: 'INFO'},\n {name: 'Success', value: 'SUCCESS'},\n {name: 'Warning', value: 'WARNING'},\n {name: 'Failure', value: 'FAILURE'}\n ],\n help: \"Select the message type to use.\"\n }\n },\n {\n key: 'bodyTemplate',\n type: 'horizontalTestNotification'\n }\n\n ],\n defaultModel: {\n eventType: null,\n appriseUrls: null,\n titleTemplate: null,\n bodyTemplate: null,\n messageType: 'WARNING'\n }\n }\n }\n ]\n\n }\n\n function notificationTemplateHelpController($scope, NotificationService) {\n $scope.model.eventTypeReadable = NotificationService.humanize($scope.model.eventType);\n $scope.to.help = NotificationService.getTemplateHelp($scope.model.eventType);\n }\n }\n}\n\nfunction handleConnectionCheckFail(ModalService, data, model, whatFailed, deferred) {\n var message;\n var yesText;\n if (data.checked) {\n message = \"The connection to the \" + whatFailed + \" failed: \" + data.message + \"
          Do you want to add it anyway?\";\n yesText = \"I know what I'm doing\";\n } else {\n message = \"The connection to the \" + whatFailed + \" could not be tested, sorry. Please check the log.\";\n yesText = \"I'll risk it\";\n }\n ModalService.open(\"Connection check failed\", message, {\n yes: {\n onYes: function () {\n deferred.resolve();\n },\n text: yesText\n },\n no: {\n onNo: function () {\n model.enabled = false;\n deferred.resolve();\n },\n text: \"Add it, but disabled\"\n },\n cancel: {\n onCancel: function () {\n deferred.reject();\n },\n text: \"Aahh, let me try again\"\n }\n });\n}\n","\nConfigController.$inject = [\"$scope\", \"$http\", \"activeTab\", \"ConfigService\", \"config\", \"DownloaderCategoriesService\", \"ConfigFields\", \"ConfigModel\", \"ModalService\", \"RestartService\", \"localStorageService\", \"$state\", \"growl\", \"$window\"];angular\n .module('nzbhydraApp')\n .factory('ConfigModel', function () {\n return {};\n });\n\nangular\n .module('nzbhydraApp')\n .factory('ConfigWatcher', function () {\n var $scope;\n\n return {\n watch: watch\n };\n\n function watch(scope) {\n $scope = scope;\n $scope.$watchGroup([\"config.main.host\"], function () {\n }, true);\n }\n });\n\n\nangular\n .module('nzbhydraApp')\n .controller('ConfigController', ConfigController);\n\nfunction ConfigController($scope, $http, activeTab, ConfigService, config, DownloaderCategoriesService, ConfigFields, ConfigModel, ModalService, RestartService, localStorageService, $state, growl, $window) {\n $scope.config = config;\n $scope.submit = submit;\n $scope.activeTab = activeTab;\n\n $scope.restartRequired = false;\n $scope.ignoreSaveNeeded = false;\n console.log(localStorageService.get(\"showAdvanced\"));\n if (localStorageService.get(\"showAdvanced\") === null) {\n $scope.showAdvanced = false;\n localStorageService.set(\"showAdvanced\", false);\n } else {\n $scope.showAdvanced = localStorageService.get(\"showAdvanced\");\n }\n\n\n $scope.toggleShowAdvanced = function () {\n $scope.showAdvanced = !$scope.showAdvanced;\n var wasDirty = $scope.form.$dirty === true;\n\n $scope.allTabs[$scope.activeTab].model.showAdvanced = $scope.showAdvanced === true;\n //Also save in main tab where it will be stored to file\n $scope.allTabs[0].model.showAdvanced = $scope.allTabs[$scope.activeTab].model.showAdvanced === true;\n $scope.form.$dirty = wasDirty;\n localStorageService.set(\"showAdvanced\", $scope.showAdvanced);\n }\n\n function updateAndAskForRestartIfNecessary(responseData) {\n if (angular.isUndefined($scope.form)) {\n console.error(\"Unable to determine if a restart is necessary\");\n return;\n }\n\n $scope.form.$setPristine();\n DownloaderCategoriesService.invalidate();\n if ($scope.restartRequired) {\n ModalService.open(\"Restart required\", \"The changes you have made may require a restart to be effective.
          Do you want to restart now?\", {\n yes: {\n onYes: function () {\n RestartService.restart();\n }\n },\n no: {\n onNo: function ($uibModalInstance) {\n //Needs to be clicked twice for some reason\n $scope.restartRequired = false;\n $uibModalInstance.dismiss();\n $uibModalInstance.dismiss();\n $scope.config = responseData.newConfig;\n $window.location.reload();\n }\n }\n });\n } else {\n $scope.config = responseData.newConfig;\n $window.location.reload();\n }\n }\n\n function handleConfigSetResponse(response, ignoreWarnings, restartNeeded) {\n if (angular.isUndefined(ignoreWarnings)) {\n ignoreWarnings = localStorageService.get(\"ignoreWarnings\") !== null ? localStorageService.get(\"ignoreWarnings\") : false;\n }\n //Communication with server was successful but there might be validation errors and/or warnings\n var warningMessages = response.data.warningMessages;\n var errorMessages = response.data.errorMessages;\n $scope.restartRequired = response.data.restartNeeded || (angular.isDefined(restartNeeded) ? restartNeeded : false);\n var showMessage = errorMessages.length > 0 || (warningMessages.length > 0 && !ignoreWarnings);\n\n function extendMessageWithList(message, messages) {\n _.forEach(messages, function (x) {\n message += \"
        • \" + x + \"
        • \";\n });\n message += \"
        \";\n return message;\n }\n\n if (showMessage) {\n var options;\n var message;\n var title;\n if (errorMessages.length > 0) { //Actual errors which cannot be ignored\n title = \"Config validation failed\";\n message = 'The following errors have been found in your config. They need to be fixed.
          ';\n message = extendMessageWithList(message, response.data.errorMessages);\n if (warningMessages.length > 0) {\n message += '
          The following warnings were found. You can ignore them if you wish.
            ';\n message = extendMessageWithList(message, response.data.warningMessages);\n }\n options = {\n yes: {\n onYes: function () {\n },\n text: \"OK\"\n }\n };\n } else if (warningMessages.length > 0) {\n title = \"Config validation warnings\";\n message = '
            The following warnings have been found. You can ignore them if you wish. The config was already saved.
              ';\n message = extendMessageWithList(message, response.data.warningMessages);\n options = {\n // cancel: {\n // onCancel: function () {\n // $scope.form.$setPristine();\n // localStorageService.set(\"ignoreWarnings\", true);\n // ConfigService.set($scope.config, true).then(function (response) {\n // handleConfigSetResponse(response, true, $scope.restartRequired);\n // updateAndAskForRestartIfNecessary(response.data);\n // }, function (response) {\n // //Actual error while setting or validating config\n // growl.error(response.data);\n // });\n // },\n // text: \"OK, don't show warnings again\"\n // },\n yes: {\n onYes: function () {\n handleConfigSetResponse(response, true, $scope.restartRequired);\n updateAndAskForRestartIfNecessary(response.data);\n },\n text: \"OK\"\n }\n };\n }\n ModalService.open(title, message, options, \"md\", \"left\");\n } else {\n updateAndAskForRestartIfNecessary(response.data);\n }\n }\n\n function submit() {\n if ($scope.form.$valid && !$scope.myShowError) {\n ConfigService.set($scope.config, true).then(function (response) {\n handleConfigSetResponse(response);\n }, function (response) {\n //Actual error while setting or validating config\n growl.error(response.data);\n });\n\n } else {\n growl.error(\"Config invalid. Please check your settings.\");\n\n //Ridiculously hacky way to make the error messages appear\n try {\n if (angular.isDefined(form.$error.required)) {\n _.each(form.$error.required, function (item) {\n if (angular.isDefined(item.$error.required)) {\n _.each(item.$error.required, function (item2) {\n item2.$setTouched();\n });\n }\n });\n }\n angular.forEach($scope.form.$error.required, function (field) {\n field.$setTouched();\n });\n } catch (err) {\n //\n }\n\n }\n }\n\n ConfigModel = config;\n\n $scope.fields = ConfigFields.getFields($scope.config);\n\n $scope.allTabs = [\n {\n active: false,\n state: 'root.config.main',\n name: 'Main',\n model: ConfigModel.main,\n fields: $scope.fields.main\n },\n {\n active: false,\n state: 'root.config.auth',\n name: 'Authorization',\n model: ConfigModel.auth,\n fields: $scope.fields.auth,\n options: {}\n },\n {\n active: false,\n state: 'root.config.searching',\n name: 'Searching',\n model: ConfigModel.searching,\n fields: $scope.fields.searching,\n options: {}\n },\n {\n active: false,\n state: 'root.config.categories',\n name: 'Categories',\n model: ConfigModel.categoriesConfig,\n fields: $scope.fields.categoriesConfig,\n options: {}\n },\n {\n active: false,\n state: 'root.config.downloading',\n name: 'Downloading',\n model: ConfigModel.downloading,\n fields: $scope.fields.downloading,\n options: {}\n },\n {\n active: false,\n state: 'root.config.indexers',\n name: 'Indexers',\n model: ConfigModel.indexers,\n fields: $scope.fields.indexers,\n options: {}\n },\n {\n active: false,\n state: 'root.config.notifications',\n name: 'Notifications',\n model: ConfigModel.notificationConfig,\n fields: $scope.fields.notificationConfig,\n options: {}\n }\n ];\n\n //Copy showAdvanced setting over from main tab's setting\n _.each($scope.allTabs, function (tab) {\n tab.model.showAdvanced = $scope.showAdvanced === true;\n })\n\n $scope.isSavingNeeded = function () {\n return $scope.form.$dirty && $scope.form.$valid && !$scope.ignoreSaveNeeded;\n };\n\n $scope.goToConfigState = function (index) {\n $state.go($scope.allTabs[index].state, {activeTab: index}, {inherit: false, notify: true, reload: true});\n };\n\n $scope.apiHelp = function () {\n\n if ($scope.isSavingNeeded()) {\n growl.info(\"Please save first\");\n return;\n }\n var apiHelp = ConfigService.apiHelp().then(function (data) {\n\n var html = '' +\n '' +\n '' +\n '' +\n '
              Newznab API endpoint:%newznab%
              Torznab API endpoint:%torznab%
              API key:%apikey%
              ';\n //Torznab API endpoint: %torznab%
              API key: %apikey%\n html = html.replace(\"%newznab%\", data.newznabApi);\n html = html.replace(\"%torznab%\", data.torznabApi);\n html = html.replace(\"%apikey%\", data.apiKey);\n ModalService.open(\"API infos\", html, {}, \"md\");\n });\n };\n\n $scope.configureIn = function (externalTool) {\n\n if ($scope.isSavingNeeded()) {\n growl.info(\"Please save first\");\n return;\n }\n ConfigService.configureIn(externalTool);\n };\n\n $scope.$on('$stateChangeStart',\n function (event, toState, toParams, fromState, fromParams) {\n if ($scope.isSavingNeeded()) {\n event.preventDefault();\n ModalService.open(\"Unsaved changed\", \"Do you want to save before leaving?\", {\n yes: {\n onYes: function () {\n $scope.submit();\n $state.go(toState);\n },\n text: \"Yes\"\n },\n no: {\n onNo: function () {\n $scope.ignoreSaveNeeded = true;\n $scope.allTabs[$scope.activeTab].options.resetModel();\n $state.go(toState);\n },\n text: \"No\"\n },\n cancel: {\n onCancel: function () {\n event.preventDefault();\n },\n text: \"Cancel\"\n }\n });\n }\n });\n\n $scope.$watch(\"$scope.form.$valid\", function () {\n });\n\n $scope.$on('$formValidity', function (event, isValid) {\n console.log(\"Received $formValidity event: \" + isValid);\n $scope.form.$valid = isValid;\n $scope.form.$invalid = !isValid;\n $scope.showError = !isValid;\n $scope.myShowError = !isValid;\n });\n}\n\n\n","\nUpdateService.$inject = [\"$http\", \"growl\", \"blockUI\", \"RestartService\", \"RequestsErrorHandler\", \"$uibModal\", \"$timeout\"];\nUpdateModalInstanceCtrl.$inject = [\"$scope\", \"$http\", \"$interval\", \"RequestsErrorHandler\"];angular\n .module('nzbhydraApp')\n .factory('UpdateService', UpdateService);\n\nfunction UpdateService($http, growl, blockUI, RestartService, RequestsErrorHandler, $uibModal, $timeout) {\n\n var currentVersion;\n var latestVersion;\n var betaVersion;\n var updateAvailable;\n var betaUpdateAvailable;\n var latestVersionIgnored;\n var betaVersionsEnabled;\n var versionHistory;\n var updatedExternally;\n var automaticUpdateToNotice;\n\n\n return {\n update: update,\n showChanges: showChanges,\n getInfos: getInfos,\n getVersionHistory: getVersionHistory,\n ignore: ignore,\n showChangesFromAutomaticUpdate: showChangesFromAutomaticUpdate\n };\n\n function getInfos() {\n return RequestsErrorHandler.specificallyHandled(function () {\n return $http.get(\"internalapi/updates/infos\").then(\n function (response) {\n currentVersion = response.data.currentVersion;\n latestVersion = response.data.latestVersion;\n betaVersion = response.data.betaVersion;\n updateAvailable = response.data.updateAvailable;\n betaUpdateAvailable = response.data.betaUpdateAvailable;\n latestVersionIgnored = response.data.latestVersionIgnored;\n betaVersionsEnabled = response.data.betaVersionsEnabled;\n updatedExternally = response.data.updatedExternally;\n automaticUpdateToNotice = response.data.automaticUpdateToNotice;\n return response;\n }, function () {\n\n }\n );\n });\n }\n\n function ignore(version) {\n return $http.put(\"internalapi/updates/ignore/\" + version).then(function (response) {\n return response;\n });\n }\n\n function getVersionHistory() {\n return $http.get(\"internalapi/updates/versionHistory\").then(function (response) {\n versionHistory = response.data;\n return response;\n });\n }\n\n function showChanges(version) {\n return $http.get(\"internalapi/updates/changesSince/\" + version).then(function (response) {\n var params = {\n size: \"lg\",\n templateUrl: \"static/html/changelog-modal.html\",\n resolve: {\n versionHistory: function () {\n return response.data;\n }\n },\n controller: function ($scope, $sce, $uibModalInstance, versionHistory) {\n $scope.versionHistory = versionHistory;\n\n $scope.ok = function () {\n $uibModalInstance.dismiss();\n };\n }\n };\n\n var modalInstance = $uibModal.open(params);\n modalInstance.result.then();\n });\n }\n\n function showChangesFromAutomaticUpdate() {\n return $http.get(\"internalapi/updates/automaticUpdateVersionHistory\").then(function (response) {\n var params = {\n size: \"lg\",\n templateUrl: \"static/html/changelog-modal.html\",\n resolve: {\n versionHistory: function () {\n return response.data;\n }\n },\n controller: function ($scope, $sce, $uibModalInstance, versionHistory) {\n $scope.versionHistory = versionHistory;\n\n $scope.ok = function () {\n $uibModalInstance.dismiss();\n };\n }\n };\n\n var modalInstance = $uibModal.open(params);\n modalInstance.result.then();\n return $http.get(\"internalapi/updates/ackAutomaticUpdateVersionHistory\").then(function (response) {\n\n });\n });\n }\n\n\n function update(version) {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/update-modal.html',\n controller: 'UpdateModalInstanceCtrl',\n size: \"md\",\n backdrop: 'static',\n keyboard: false\n });\n $http.put(\"internalapi/updates/installUpdate/\" + version).then(function () {\n //Handle like restart, ping application and wait\n //Perhaps save the version to which we want to update, ask later and see if they're equal. If not updating apparently failed...\n $timeout(function () {\n //Give user some time to read the last message\n RestartService.startCountdown(\"\");\n modalInstance.close();\n }, 2000);\n },\n function () {\n growl.info(\"An error occurred while updating. Please check the logs.\");\n modalInstance.close();\n });\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('UpdateModalInstanceCtrl', UpdateModalInstanceCtrl);\n\nfunction UpdateModalInstanceCtrl($scope, $http, $interval, RequestsErrorHandler) {\n $scope.messages = [];\n\n var interval = $interval(function () {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get(\"internalapi/updates/messages\").then(\n function (data) {\n $scope.messages = data.data;\n }\n );\n });\n },\n 200);\n\n $scope.$on('$destroy', function () {\n if (interval !== null) {\n $interval.cancel(interval);\n }\n });\n\n}\n","\nSystemController.$inject = [\"$scope\", \"$state\", \"activeTab\", \"simpleInfos\", \"$http\", \"growl\", \"RestartService\", \"MigrationService\", \"ConfigService\", \"NzbHydraControlService\", \"RequestsErrorHandler\"];angular\n .module('nzbhydraApp')\n .controller('SystemController', SystemController);\n\nfunction SystemController($scope, $state, activeTab, simpleInfos, $http, growl, RestartService, MigrationService, ConfigService, NzbHydraControlService, RequestsErrorHandler) {\n\n $scope.activeTab = activeTab;\n $scope.foo = {\n csv: \"\",\n sql: \"\"\n };\n\n $scope.simpleInfos = simpleInfos;\n\n $scope.shutdown = function () {\n NzbHydraControlService.shutdown().then(function () {\n growl.info(\"Shutdown initiated. Cya!\");\n },\n function () {\n growl.info(\"Unable to send shutdown command.\");\n })\n };\n\n $scope.restart = function () {\n RestartService.restart();\n };\n\n $scope.reloadConfig = function () {\n ConfigService.reloadConfig().then(function () {\n growl.info(\"Successfully reloaded config. Some setting may need a restart to take effect.\")\n }, function (data) {\n growl.error(data.message);\n })\n };\n\n\n $scope.migrate = function () {\n MigrationService.migrate();\n };\n\n\n $scope.allTabs = [\n {\n active: false,\n state: 'root.system.control',\n name: \"Control\"\n },\n {\n active: false,\n state: 'root.system.updates',\n name: \"Updates\"\n },\n {\n active: false,\n state: 'root.system.log',\n name: \"Log\"\n },\n {\n active: false,\n state: 'root.system.tasks',\n name: \"Tasks\"\n },\n {\n active: false,\n state: 'root.system.backup',\n name: \"Backup\"\n },\n {\n active: false,\n state: 'root.system.bugreport',\n name: \"Bugreport / Debug\"\n },\n {\n active: false,\n state: 'root.system.news',\n name: \"News\"\n },\n {\n active: false,\n state: 'root.system.about',\n name: \"About\"\n }\n ];\n\n\n $scope.goToSystemState = function (index) {\n $state.go($scope.allTabs[index].state, {activeTab: index}, {inherit: false, notify: true, reload: true});\n };\n\n $scope.downloadDebuggingInfos = function () {\n $scope.isBackupCreationAction = true;\n $http({\n method: 'GET',\n url: 'internalapi/debuginfos/createAndProvideZipAsBytes',\n responseType: 'arraybuffer'\n }).then(function (response, status, headers, config) {\n var a = document.createElement('a');\n var blob = new Blob([response.data], {'type': \"application/octet-stream\"});\n a.href = URL.createObjectURL(blob);\n a.download = \"nzbhydra-debuginfos-\" + moment().format(\"YYYY-MM-DD-HH-mm\") + \".zip\";\n\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n $scope.isBackupCreationAction = false;\n });\n };\n\n $scope.uploadDebuggingInfos = function () {\n $scope.isBackupCreationAction = true;\n $http({\n method: 'GET',\n url: 'internalapi/debuginfos/createAndUploadDebugInfos'\n }).then(function (response) {\n $scope.debugInfosUrl = 'URL with debug infos: ' + response.data + '';\n $scope.isBackupCreationAction = false;\n }, function (response) {\n $scope.debugInfosUrl = response.data;\n $scope.isBackupCreationAction = false;\n });\n };\n\n $scope.logThreadDump = function () {\n $http({\n method: 'GET',\n url: 'internalapi/debuginfos/logThreadDump'\n });\n };\n\n $scope.executeSqlQuery = function () {\n $http.post('internalapi/debuginfos/executesqlquery', $scope.foo.sql).then(function (response) {\n if (response.data.successful) {\n $scope.foo.csv = response.data.message;\n } else {\n growl.error(response.data.message);\n }\n });\n };\n\n $scope.executeSqlUpdate = function () {\n $http.post('internalapi/debuginfos/executesqlupdate', $scope.foo.sql).then(function (response) {\n if (response.data.successful) {\n $scope.foo.csv = response.data.message + \" rows affected\";\n } else {\n growl.error(response.data.message);\n }\n });\n };\n\n\n $scope.cpuChart = {\n options: {\n chart:\n {\n type: 'lineChart',\n height: 450,\n margin: {\n top: 20,\n right: 20,\n bottom: 60,\n left: 65\n },\n x: function (d) {\n return d.time;\n },\n y: function (d) {\n return d.value;\n },\n xAxis: {\n axisLabel: 'Time',\n tickFormat: function (d) {\n return moment.unix(d).local().format(\"HH:mm:ss\");\n },\n showMaxMin: true\n },\n\n yAxis: {\n axisLabel: 'CPU %'\n },\n interactive: true\n }\n },\n data: []\n };\n\n function update() {\n RequestsErrorHandler.specificallyHandled(function () {\n $http.get(\"internalapi/debuginfos/threadCpuUsage\", {ignoreLoadingBar: true}).then(function (response) {\n try {\n if (!response) {\n console.error(\"No CPU usage data from server\");\n return;\n }\n $scope.cpuChart.data = response.data;\n\n } catch (e) {\n console.error(e);\n clearInterval(timer);\n }\n },\n function () {\n console.error(\"Error while loading CPU usage data status\");\n clearInterval(timer);\n }\n );\n });\n }\n\n $scope.cpuChart.data = [];\n\n update();\n var timer = setInterval(function () {\n update();\n }, 5000);\n\n $scope.$on('$destroy', function () {\n if (timer !== null) {\n clearInterval(timer);\n }\n });\n\n}\n","\r\nStatsService.$inject = [\"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('StatsService', StatsService);\r\n\r\nfunction StatsService($http) {\r\n\r\n return {\r\n get: getStats,\r\n getDownloadHistory: getDownloadHistory,\r\n getNotificationHistory: getNotificationHistory\r\n };\r\n\r\n function getStats(after, before, includeDisabled, switchState) {\r\n var requestBody = {after: after, before: before, includeDisabled: includeDisabled};\r\n requestBody = _.extend(requestBody, switchState);\r\n return $http.post(\"internalapi/stats\", requestBody).then(function (response) {\r\n return response.data;\r\n });\r\n }\r\n\r\n function buildParams(pageNumber, limit, filterModel, sortModel) {\r\n var params = {page: pageNumber, limit: limit, filterModel: filterModel};\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n } else {\r\n params.sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n }\r\n return params;\r\n }\r\n\r\n function getDownloadHistory(pageNumber, limit, filterModel, sortModel) {\r\n var params = buildParams(pageNumber, limit, filterModel, sortModel);\r\n return $http.post(\"internalapi/history/downloads\", params).then(function (response) {\r\n return {\r\n nzbDownloads: response.data.content,\r\n totalDownloads: response.data.totalElements\r\n };\r\n\r\n });\r\n }\r\n\r\n function getNotificationHistory(pageNumber, limit, filterModel, sortModel) {\r\n var params = buildParams(pageNumber, limit, filterModel, sortModel);\r\n return $http.post(\"internalapi/history/notifications\", params).then(function (response) {\r\n return {\r\n notifications: response.data.content,\r\n totalNotifications: response.data.totalElements\r\n };\r\n\r\n });\r\n }\r\n\r\n}","\r\nStatsController.$inject = [\"$scope\", \"$filter\", \"StatsService\", \"blockUI\", \"localStorageService\", \"$timeout\", \"$window\", \"ConfigService\"];angular\r\n .module('nzbhydraApp')\r\n .controller('StatsController', StatsController);\r\n\r\nfunction StatsController($scope, $filter, StatsService, blockUI, localStorageService, $timeout, $window, ConfigService) {\r\n\r\n $scope.dateOptions = {\r\n dateDisabled: false,\r\n formatYear: 'yy',\r\n startingDay: 1\r\n };\r\n var initializingAfter = true;\r\n var initializingBefore = true;\r\n $scope.afterDate = moment().subtract(30, \"days\").toDate();\r\n $scope.beforeDate = moment().add(1, \"days\").toDate();\r\n var historyInfoTypeUserEnabled = ConfigService.getSafe().logging.historyUserInfoType === 'USERNAME' || ConfigService.getSafe().logging.historyUserInfoType === 'BOTH';\r\n var historyInfoTypeIpEnabled = ConfigService.getSafe().logging.historyUserInfoType === 'IP' || ConfigService.getSafe().logging.historyUserInfoType === 'BOTH';\r\n $scope.foo = {\r\n includeDisabledIndexersInStats: localStorageService.get(\"includeDisabledIndexersInStats\") !== null ? localStorageService.get(\"includeDisabledIndexersInStats\") : false,\r\n statsSwichState: localStorageService.get(\"statsSwitchState\") !== null ? localStorageService.get(\"statsSwitchState\") :\r\n {\r\n indexerApiAccessStats: true,\r\n avgIndexerUniquenessScore: true,\r\n avgResponseTimes: true,\r\n indexerDownloadShares: true,\r\n downloadsPerDayOfWeek: true,\r\n downloadsPerHourOfDay: true,\r\n searchesPerDayOfWeek: true,\r\n searchesPerHourOfDay: true,\r\n downloadsPerAgeStats: true,\r\n successfulDownloadsPerIndexer: true,\r\n downloadSharesPerUser: historyInfoTypeUserEnabled,\r\n searchSharesPerUser: historyInfoTypeIpEnabled,\r\n downloadSharesPerIp: true,\r\n searchSharesPerIp: true,\r\n userAgentSearchShares: true,\r\n userAgentDownloadShares: true\r\n }\r\n };\r\n localStorageService.set(\"statsSwitchState\", $scope.foo.statsSwichState);\r\n $scope.stats = {};\r\n\r\n updateStats();\r\n\r\n\r\n $scope.openAfter = function () {\r\n $scope.after.opened = true;\r\n };\r\n\r\n $scope.openBefore = function () {\r\n $scope.before.opened = true;\r\n };\r\n\r\n $scope.after = {\r\n opened: false\r\n };\r\n\r\n $scope.before = {\r\n opened: false\r\n };\r\n\r\n $scope.toggleIncludeDisabledIndexers = function () {\r\n localStorageService.set(\"includeDisabledIndexersInStats\", $scope.foo.includeDisabledIndexersInStats);\r\n };\r\n\r\n $scope.onStatsSwitchToggle = function (statId) {\r\n localStorageService.set(\"statsSwitchState\", $scope.foo.statsSwichState);\r\n\r\n if ($scope.foo.statsSwichState[statId]) { //Stat was enabled, get only data for this stat\r\n updateStats(statId);\r\n }\r\n\r\n };\r\n\r\n $scope.refresh = function () {\r\n updateStats();\r\n };\r\n\r\n function updateStats(statId) {\r\n blockUI.start(\"Updating stats...\");\r\n var after = $scope.afterDate !== null ? $scope.afterDate : null;\r\n var before = $scope.beforeDate !== null ? $scope.beforeDate : null;\r\n var statsToRetrieve = {};\r\n if (angular.isDefined(statId)) {\r\n statsToRetrieve[statId] = true;\r\n } else {\r\n statsToRetrieve = $scope.foo.statsSwichState;\r\n }\r\n $scope.statsLoadingPromise = StatsService.get(after, before, $scope.foo.includeDisabledIndexersInStats, statsToRetrieve).then(function (stats) {\r\n $scope.setStats(stats);\r\n //Resize event is needed for the -perUsernameOrIp charts to be properly sized because nvd3 thinks the initial size is 0\r\n $timeout(function () {\r\n $window.dispatchEvent(new Event(\"resize\"));\r\n }, 500);\r\n });\r\n\r\n blockUI.reset();\r\n }\r\n\r\n $scope.$watch('beforeDate', function () {\r\n if (initializingBefore) {\r\n initializingBefore = false;\r\n } else {\r\n //updateStats();\r\n }\r\n });\r\n\r\n\r\n $scope.$watch('afterDate', function () {\r\n if (initializingAfter) {\r\n initializingAfter = false;\r\n } else {\r\n //updateStats();\r\n }\r\n });\r\n\r\n $scope.onKeypress = function (keyEvent) {\r\n if (keyEvent.which === 13) {\r\n //updateStats();\r\n }\r\n };\r\n\r\n\r\n $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];\r\n $scope.format = $scope.formats[0];\r\n $scope.altInputFormats = ['M!/d!/yyyy'];\r\n\r\n $scope.setStats = function (stats) {\r\n //Only update those stats that were calculated (because this might be an update when one stat has just been enabled)\r\n _.forEach(stats, function (value, key) {\r\n if (value !== null) {\r\n $scope.stats[key] = value;\r\n }\r\n });\r\n\r\n if ($scope.stats.avgResponseTimes) {\r\n $scope.avgResponseTimesChart = getChart(\"multiBarHorizontalChart\", $scope.stats.avgResponseTimes, \"indexer\", \"avgResponseTime\", \"\", \"Response time (ms)\");\r\n $scope.avgResponseTimesChart.options.chart.margin.left = 100;\r\n $scope.avgResponseTimesChart.options.chart.yAxis.rotateLabels = -30;\r\n $scope.avgResponseTimesChart.options.chart.height = Math.max($scope.stats.avgResponseTimes.length * 30, 350);\r\n }\r\n\r\n if ($scope.stats.downloadsPerHourOfDay) {\r\n $scope.downloadsPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.stats.downloadsPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Downloads');\r\n $scope.downloadsPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.downloadsPerDayOfWeek) {\r\n $scope.downloadsPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.stats.downloadsPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Downloads');\r\n $scope.downloadsPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.searchesPerHourOfDay) {\r\n $scope.searchesPerHourOfDayChart = getChart(\"discreteBarChart\", $scope.stats.searchesPerHourOfDay, \"hour\", \"count\", \"Hour of day\", 'Searches');\r\n $scope.searchesPerHourOfDayChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.searchesPerDayOfWeek) {\r\n $scope.searchesPerDayOfWeekChart = getChart(\"discreteBarChart\", $scope.stats.searchesPerDayOfWeek, \"day\", \"count\", \"Day of week\", 'Searches');\r\n $scope.searchesPerDayOfWeekChart.options.chart.xAxis.rotateLabels = 0;\r\n }\r\n\r\n if ($scope.stats.downloadsPerAgeStats) {\r\n $scope.downloadsPerAgeChart = getChart(\"discreteBarChart\", $scope.stats.downloadsPerAgeStats.downloadsPerAge, \"age\", \"count\", \"Downloads per age\", 'Downloads');\r\n $scope.downloadsPerAgeChart.options.chart.xAxis.rotateLabels = 45;\r\n $scope.downloadsPerAgeChart.options.chart.showValues = false;\r\n }\r\n\r\n if ($scope.stats.successfulDownloadsPerIndexer) {\r\n $scope.successfulDownloadsPerIndexerChart = getChart(\"multiBarHorizontalChart\", $scope.stats.successfulDownloadsPerIndexer, \"indexerName\", \"percentSuccessful\", \"Indexer\", '% successful');\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.xAxis.rotateLabels = 90;\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.yAxis.tickFormat = function (d) {\r\n return $filter('number')(d, 0);\r\n };\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.valueFormat = function (d) {\r\n return $filter('number')(d, 0);\r\n };\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.showValues = true;\r\n $scope.successfulDownloadsPerIndexerChart.options.chart.margin.left = 80;\r\n }\r\n\r\n if ($scope.stats.indexerDownloadShares) {\r\n $scope.indexerDownloadSharesChart = {\r\n options: {\r\n chart: {\r\n type: 'pieChart',\r\n height: 500,\r\n x: function (d) {\r\n return d.indexerName;\r\n },\r\n y: function (d) {\r\n return d.share;\r\n },\r\n showLabels: true,\r\n donut: true,\r\n donutRatio: 0.35,\r\n duration: 500,\r\n labelThreshold: 0.03,\r\n labelSunbeamLayout: true,\r\n tooltip: {\r\n valueFormatter: function (d, i) {\r\n return $filter('number')(d, 2) + \"%\";\r\n }\r\n },\r\n legend: {\r\n margin: {\r\n top: 5,\r\n right: 35,\r\n bottom: 5,\r\n left: 0\r\n }\r\n }\r\n }\r\n },\r\n data: $scope.stats.indexerDownloadShares\r\n };\r\n $scope.indexerDownloadSharesChart.options.chart.height = Math.min(Math.max(($scope.foo.includeDisabledIndexersInStats ? $scope.stats.numberOfConfiguredIndexers : $scope.stats.numberOfEnabledIndexers) * 40, 350), 900);\r\n }\r\n\r\n function getSharesPieChart(data, height, xValue, yValue) {\r\n return {\r\n options: {\r\n chart: {\r\n type: 'pieChart',\r\n height: height,\r\n x: function (d) {\r\n return d[xValue];\r\n },\r\n y: function (d) {\r\n return d[yValue];\r\n },\r\n showLabels: true,\r\n donut: true,\r\n donutRatio: 0.35,\r\n duration: 500,\r\n labelThreshold: 0.03,\r\n labelsOutside: true,\r\n //labelType: \"percent\",\r\n labelSunbeamLayout: true,\r\n tooltip: {\r\n valueFormatter: function (d, i) {\r\n return $filter('number')(d, 2) + \"%\";\r\n }\r\n },\r\n legend: {\r\n margin: {\r\n top: 5,\r\n right: 35,\r\n bottom: 5,\r\n left: 0\r\n }\r\n }\r\n }\r\n },\r\n data: data\r\n };\r\n }\r\n\r\n if ($scope.stats.searchSharesPerIp !== null) {\r\n $scope.downloadSharesPerIpChart = getSharesPieChart($scope.stats.downloadSharesPerIp, 300, \"key\", \"percentage\");\r\n }\r\n if ($scope.stats.searchSharesPerIpChart !== null) {\r\n $scope.searchSharesPerIpChart = getSharesPieChart($scope.stats.searchSharesPerIp, 300, \"key\", \"percentage\");\r\n }\r\n if ($scope.stats.searchSharesPerUser !== null) {\r\n $scope.downloadSharesPerUserChart = getSharesPieChart($scope.stats.downloadSharesPerUser, 300, \"key\", \"percentage\");\r\n }\r\n if ($scope.stats.searchSharesPerUserChart !== null) {\r\n $scope.searchSharesPerUserChart = getSharesPieChart($scope.stats.searchSharesPerUser, 300, \"key\", \"percentage\");\r\n }\r\n\r\n if ($scope.stats.userAgentSearchShares) {\r\n $scope.userAgentSearchSharesChart = getSharesPieChart($scope.stats.userAgentSearchShares, 300, \"userAgent\", \"percentage\");\r\n $scope.userAgentSearchSharesChart.options.chart.legend.margin.bottom = 25;\r\n }\r\n if ($scope.stats.userAgentDownloadShares) {\r\n $scope.userAgentDownloadSharesChart = getSharesPieChart($scope.stats.userAgentDownloadShares, 300, \"userAgent\", \"percentage\");\r\n $scope.userAgentDownloadSharesChart.options.chart.legend.margin.bottom = 25;\r\n }\r\n\r\n };\r\n\r\n function getChart(chartType, values, xKey, yKey, xAxisLabel, yAxisLabel) {\r\n return {\r\n options: {\r\n chart: {\r\n type: chartType,\r\n height: 350,\r\n margin: {\r\n top: 20,\r\n right: 20,\r\n bottom: 100,\r\n left: 50\r\n },\r\n x: function (d) {\r\n return d[xKey];\r\n },\r\n y: function (d) {\r\n return d[yKey];\r\n },\r\n showValues: true,\r\n valueFormat: function (d) {\r\n return d;\r\n },\r\n color: function () {\r\n return \"red\"\r\n },\r\n showControls: false,\r\n showLegend: false,\r\n duration: 100,\r\n xAxis: {\r\n axisLabel: xAxisLabel,\r\n tickFormat: function (d) {\r\n return d;\r\n },\r\n rotateLabels: 30,\r\n showMaxMin: false,\r\n color: function () {\r\n return \"white\"\r\n }\r\n },\r\n yAxis: {\r\n axisLabel: yAxisLabel,\r\n axisLabelDistance: -10,\r\n tickFormat: function (d) {\r\n return d;\r\n }\r\n },\r\n tooltip: {\r\n enabled: false\r\n },\r\n zoom: {\r\n enabled: true,\r\n scaleExtent: [1, 10],\r\n useFixedDomain: false,\r\n useNiceScale: false,\r\n horizontalOff: false,\r\n verticalOff: true,\r\n unzoomEventType: 'dblclick.zoom'\r\n }\r\n }\r\n }, data: [{\r\n \"key\": \"doesntmatter\",\r\n \"bar\": true,\r\n \"values\": values\r\n }]\r\n };\r\n }\r\n}\r\n","//\nSearchService.$inject = [\"$http\"];\nangular\n .module('nzbhydraApp')\n .factory('SearchService', SearchService);\n\nfunction SearchService($http) {\n\n\n var lastExecutedQuery;\n var lastExecutedSearchRequestParameters;\n var lastResults;\n var modalInstance;\n\n return {\n search: search,\n getLastResults: getLastResults,\n loadMore: loadMore,\n getModalInstance: getModalInstance,\n setModalInstance: setModalInstance,\n };\n\n function getModalInstance() {\n return modalInstance;\n }\n\n function setModalInstance(mi) {\n modalInstance = mi;\n }\n\n function search(searchRequestId, category, query, metaData, season, episode, minsize, maxsize, minage, maxage, indexers, mode) {\n // console.time(\"search\");\n var uri = new URI(\"internalapi/search\");\n var searchRequestParameters = {};\n searchRequestParameters.searchRequestId = searchRequestId;\n searchRequestParameters.query = query;\n searchRequestParameters.minsize = minsize;\n searchRequestParameters.maxsize = maxsize;\n searchRequestParameters.minage = minage;\n searchRequestParameters.maxage = maxage;\n searchRequestParameters.category = category;\n searchRequestParameters.mode = mode;\n if (!angular.isUndefined(indexers) && indexers !== null) {\n searchRequestParameters.indexers = indexers.split(\",\");\n }\n\n if (metaData) {\n searchRequestParameters.title = metaData.title;\n if (category.indexOf(\"Movies\") > -1 || (category.indexOf(\"20\") === 0) || mode === \"movie\") {\n searchRequestParameters.tmdbId = metaData.tmdbId;\n searchRequestParameters.imdbId = metaData.imdbId;\n } else if (category.indexOf(\"TV\") > -1 || (category.indexOf(\"50\") === 0) || mode === \"tvsearch\") {\n searchRequestParameters.tvdbId = metaData.tvdbId;\n searchRequestParameters.tvrageId = metaData.rid;\n searchRequestParameters.tvmazeId = metaData.tvmazeId;\n searchRequestParameters.season = season;\n searchRequestParameters.episode = episode;\n }\n }\n\n lastExecutedQuery = uri;\n lastExecutedSearchRequestParameters = searchRequestParameters;\n return $http.post(uri.toString(), searchRequestParameters).then(processData);\n }\n\n function loadMore(offset, limit, loadAll) {\n lastExecutedSearchRequestParameters.offset = offset;\n lastExecutedSearchRequestParameters.limit = limit;\n lastExecutedSearchRequestParameters.loadAll = angular.isDefined(loadAll) ? loadAll : false;\n\n return $http.post(lastExecutedQuery.toString(), lastExecutedSearchRequestParameters).then(processData);\n }\n\n\n function processData(response) {\n var searchResults = response.data.searchResults;\n var indexerSearchMetaDatas = response.data.indexerSearchMetaDatas;\n var numberOfAvailableResults = response.data.numberOfAvailableResults;\n var numberOfRejectedResults = response.data.numberOfRejectedResults;\n var numberOfDuplicateResults = response.data.numberOfDuplicateResults;\n var numberOfAcceptedResults = response.data.numberOfAcceptedResults;\n var numberOfProcessedResults = response.data.numberOfProcessedResults;\n var rejectedReasonsMap = response.data.rejectedReasonsMap;\n var notPickedIndexersWithReason = response.data.notPickedIndexersWithReason;\n\n lastResults = {\n \"searchResults\": searchResults,\n \"indexerSearchMetaDatas\": indexerSearchMetaDatas,\n \"numberOfAvailableResults\": numberOfAvailableResults,\n \"numberOfAcceptedResults\": numberOfAcceptedResults,\n \"numberOfRejectedResults\": numberOfRejectedResults,\n \"numberOfProcessedResults\": numberOfProcessedResults,\n \"numberOfDuplicateResults\": numberOfDuplicateResults,\n \"rejectedReasonsMap\": rejectedReasonsMap,\n \"notPickedIndexersWithReason\": notPickedIndexersWithReason\n\n };\n // console.timeEnd(\"searchonly\");\n return lastResults;\n }\n\n function getLastResults() {\n return lastResults;\n }\n}","\nSearchResultsController.$inject = [\"$stateParams\", \"$scope\", \"$q\", \"$timeout\", \"$document\", \"blockUI\", \"growl\", \"localStorageService\", \"SearchService\", \"ConfigService\", \"CategoriesService\", \"DebugService\", \"GenericStorageService\", \"ModalService\", \"$uibModal\"];angular\n .module('nzbhydraApp')\n .controller('SearchResultsController', SearchResultsController);\n\n//SearchResultsController.$inject = ['blockUi'];\nfunction SearchResultsController($stateParams, $scope, $q, $timeout, $document, blockUI, growl, localStorageService, SearchService, ConfigService, CategoriesService, DebugService, GenericStorageService, ModalService, $uibModal) {\n // console.time(\"Presenting\");\n DebugService.log(\"foobar\");\n $scope.limitTo = ConfigService.getSafe().searching.loadLimitInternal;\n $scope.offset = 0;\n $scope.allowZipDownload = ConfigService.getSafe().downloading.fileDownloadAccessType === 'PROXY';\n\n var indexerColors = {};\n\n _.each(ConfigService.getSafe().indexers, function (indexer) {\n indexerColors[indexer.name] = indexer.color;\n });\n\n //Handle incoming data\n\n $scope.indexersearches = SearchService.getLastResults().indexerSearchMetaDatas;\n $scope.notPickedIndexersWithReason = [];\n _.forEach(SearchService.getLastResults().notPickedIndexersWithReason, function (k, v) {\n $scope.notPickedIndexersWithReason.push({\"indexer\": v, \"reason\": k});\n });\n $scope.indexerResultsInfo = {}; //Stores information about the indexerName's searchResults like how many we already retrieved\n $scope.groupExpanded = {};\n $scope.selected = [];\n if ($stateParams.title) {\n $scope.searchTitle = $stateParams.title;\n } else if ($stateParams.query) {\n $scope.searchTitle = $stateParams.query;\n } else {\n $scope.searchTitle = undefined;\n }\n\n $scope.selectedIds = _.map($scope.selected, function (value) {\n return value.searchResultId;\n });\n\n //For shift clicking results\n $scope.lastClickedRowIndex = null;\n $scope.lastClickedValue = null;\n\n var allSearchResults = [];\n var sortModel = {};\n $scope.filterModel = {};\n\n\n $scope.filterButtonsModel = {\n source: {},\n quality: {},\n other: {},\n custom: {}\n };\n $scope.customFilterButtons = [];\n\n $scope.filterButtonsModelMap = {\n tv: ['hdtv'],\n camts: ['cam', 'ts'],\n web: ['webrip', 'web-dl', 'webdl'],\n dvd: ['dvd'],\n bluray: ['bluray', 'blu-ray']\n };\n _.each(ConfigService.getSafe().searching.customQuickFilterButtons, function (entry) {\n var split1 = entry.split(\"=\");\n var displayName = split1[0];\n $scope.filterButtonsModelMap[displayName] = split1[1].split(\",\");\n $scope.customFilterButtons.push(displayName);\n });\n _.each(ConfigService.getSafe().searching.preselectQuickFilterButtons, function (entry) {\n var split1 = entry.split(\"|\");\n var category = split1[0];\n var id = split1[1];\n if (category !== 'source' || $scope.isShowFilterButtonsVideo) {\n $scope.filterButtonsModel[category][id] = true;\n }\n })\n\n $scope.numberOfFilteredResults = 0;\n\n\n if ($stateParams.sortby !== undefined) {\n $stateParams.sortby = $stateParams.sortby.toLowerCase();\n sortModel = {};\n sortModel.reversed = false;\n if ($stateParams.sortby === \"title\") {\n sortModel.column = \"title\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"indexer\") {\n sortModel.column = \"indexer\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"category\") {\n sortModel.column = \"category\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"size\") {\n sortModel.column = \"size\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"details\") {\n sortModel.column = \"grabs\";\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 1;\n } else {\n sortModel.sortMode = 2;\n }\n } else if ($stateParams.sortby === \"age\") {\n sortModel.column = \"epoch\";\n sortModel.reversed = true;\n if ($stateParams.sortdirection === \"asc\" || $stateParams.sortdirection === undefined) {\n sortModel.sortMode = 2;\n } else {\n sortModel.sortMode = 1;\n }\n }\n\n\n } else if (localStorageService.get(\"sorting\") !== null) {\n sortModel = localStorageService.get(\"sorting\");\n } else {\n sortModel = {\n column: \"epoch\",\n sortMode: 2,\n reversed: false\n };\n }\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode, sortModel.reversed);\n }, 10);\n\n\n $scope.foo = {\n indexerStatusesExpanded: localStorageService.get(\"indexerStatusesExpanded\") !== null ? localStorageService.get(\"indexerStatusesExpanded\") : false,\n duplicatesDisplayed: localStorageService.get(\"duplicatesDisplayed\") !== null ? localStorageService.get(\"duplicatesDisplayed\") : false,\n groupTorrentAndNewznabResults: localStorageService.get(\"groupTorrentAndNewznabResults\") !== null ? localStorageService.get(\"groupTorrentAndNewznabResults\") : false,\n sumGrabs: localStorageService.get(\"sumGrabs\") !== null ? localStorageService.get(\"sumGrabs\") : true,\n scrollToResults: localStorageService.get(\"scrollToResults\") !== null ? localStorageService.get(\"scrollToResults\") : true,\n showCovers: localStorageService.get(\"showCovers\") !== null ? localStorageService.get(\"showCovers\") : true,\n groupEpisodes: localStorageService.get(\"groupEpisodes\") !== null ? localStorageService.get(\"groupEpisodes\") : true,\n expandGroupsByDefault: localStorageService.get(\"expandGroupsByDefault\") !== null ? localStorageService.get(\"expandGroupsByDefault\") : false,\n showDownloadedIndicator: localStorageService.get(\"showDownloadedIndicator\") !== null ? localStorageService.get(\"showDownloadedIndicator\") : true,\n hideAlreadyDownloadedResults: localStorageService.get(\"hideAlreadyDownloadedResults\") !== null ? localStorageService.get(\"hideAlreadyDownloadedResults\") : true,\n showResultsAsZipButton: localStorageService.get(\"showResultsAsZipButton\") !== null ? localStorageService.get(\"showResultsAsZipButton\") : true,\n alwaysShowTitles: localStorageService.get(\"alwaysShowTitles\") !== null ? localStorageService.get(\"alwaysShowTitles\") : true\n };\n\n\n $scope.isShowFilterButtons = ConfigService.getSafe().searching.showQuickFilterButtons;\n $scope.isShowFilterButtonsVideo = $scope.isShowFilterButtons && ($stateParams.category.toLowerCase().indexOf(\"tv\") > -1 || $stateParams.category.toLowerCase().indexOf(\"movie\") > -1 || ConfigService.getSafe().searching.alwaysShowQuickFilterButtons);\n $scope.isShowCustomFilterButtons = ConfigService.getSafe().searching.customQuickFilterButtons.length > 0;\n\n $scope.shared = {\n isGroupEpisodes: $scope.foo.groupEpisodes && $stateParams.category.toLowerCase().indexOf(\"tv\") > -1 && $stateParams.episode === undefined,\n expandGroupsByDefault: $scope.foo.expandGroupsByDefault,\n showDownloadedIndicator: $scope.foo.showDownloadedIndicator,\n hideAlreadyDownloadedResults: $scope.foo.hideAlreadyDownloadedResults,\n alwaysShowTitles: $scope.foo.alwaysShowTitles\n };\n\n if ($scope.shared.isGroupEpisodes) {\n GenericStorageService.get(\"isGroupEpisodesHelpShown\", true).then(function (response) {\n if (!response.data) {\n ModalService.open(\"Sorting of TV episodes\", 'When searching in the TV categories results are automatically grouped by episodes. This makes it easier to download one episode each. You can disable this feature any time using the \"Display options\" button to the upper left.', {\n yes: {\n text: \"OK\"\n }\n });\n GenericStorageService.put(\"isGroupEpisodesHelpShown\", true, true);\n }\n\n })\n }\n\n $scope.loadMoreEnabled = false;\n $scope.totalAvailableUnknown = false;\n $scope.expandedTitlegroups = [];\n $scope.optionsOptions = [\n {id: \"duplicatesDisplayed\", label: \"Show duplicate display triggers\"},\n {id: \"groupTorrentAndNewznabResults\", label: \"Group torrent and usenet results\"},\n {id: \"sumGrabs\", label: \"Use sum of grabs / seeders for filtering / sorting of groups\"},\n {id: \"scrollToResults\", label: \"Scroll to results when finished\"},\n {id: \"showCovers\", label: \"Show movie covers in results\"},\n {id: \"groupEpisodes\", label: \"Group TV results by season/episode\"},\n {id: \"expandGroupsByDefault\", label: \"Expand groups by default\"},\n {id: \"alwaysShowTitles\", label: \"Always show result titles (even when grouped)\"},\n {id: \"showDownloadedIndicator\", label: \"Show already downloaded indicator\"},\n {id: \"hideAlreadyDownloadedResults\", label: \"Hide already downloaded results\"}\n ];\n if ($scope.allowZipDownload) {\n $scope.optionsOptions.push({id: \"showResultsAsZipButton\", label: \"Show button to download results as ZIP\"});\n }\n $scope.optionsSelectedModel = [];\n for (var key in $scope.optionsOptions) {\n if ($scope.foo[$scope.optionsOptions[key][\"id\"]]) {\n $scope.optionsSelectedModel.push($scope.optionsOptions[key].id);\n }\n }\n\n $scope.optionsExtraSettings = {\n showSelectAll: false,\n showDeselectAll: false,\n buttonText: \"Display options\"\n };\n\n $scope.optionsEvents = {\n onToggleItem: function (item, newValue) {\n if (item.id === \"duplicatesDisplayed\") {\n toggleDuplicatesDisplayed(newValue);\n } else if (item.id === \"groupTorrentAndNewznabResults\") {\n toggleGroupTorrentAndNewznabResults(newValue);\n } else if (item.id === \"sumGrabs\") {\n toggleSumGrabs(newValue);\n } else if (item.id === \"scrollToResults\") {\n toggleScrollToResults(newValue);\n } else if (item.id === \"showCovers\") {\n toggleShowCovers(newValue);\n } else if (item.id === \"groupEpisodes\") {\n toggleGroupEpisodes(newValue);\n } else if (item.id === \"expandGroupsByDefault\") {\n toggleExpandGroups(newValue);\n } else if (item.id === \"showDownloadedIndicator\") {\n toggleDownloadedIndicator(newValue);\n } else if (item.id === \"hideAlreadyDownloadedResults\") {\n toggleHideAlreadyDownloadedResults(newValue);\n } else if (item.id === \"showResultsAsZipButton\") {\n toggleShowResultsAsZipButton(newValue);\n } else if (item.id === \"alwaysShowTitles\") {\n toggleAlwaysShowTitles(newValue);\n }\n }\n };\n\n function toggleDuplicatesDisplayed(value) {\n localStorageService.set(\"duplicatesDisplayed\", value);\n $scope.$broadcast(\"duplicatesDisplayed\", value);\n $scope.foo.duplicatesDisplayed = value;\n $scope.shared.duplicatesDisplayed = value;\n }\n\n function toggleGroupTorrentAndNewznabResults(value) {\n localStorageService.set(\"groupTorrentAndNewznabResults\", value);\n $scope.foo.groupTorrentAndNewznabResults = value;\n $scope.shared.groupTorrentAndNewznabResults = value;\n blockAndUpdate();\n }\n\n function toggleSumGrabs(value) {\n localStorageService.set(\"sumGrabs\", value);\n $scope.foo.sumGrabs = value;\n $scope.shared.sumGrabs = value;\n blockAndUpdate();\n }\n\n function toggleScrollToResults(value) {\n localStorageService.set(\"scrollToResults\", value);\n $scope.foo.scrollToResults = value;\n $scope.shared.scrollToResults = value;\n }\n\n function toggleShowCovers(value) {\n localStorageService.set(\"showCovers\", value);\n $scope.foo.showCovers = value;\n $scope.shared.showCovers = value;\n $scope.$broadcast(\"toggleShowCovers\", value);\n }\n\n function toggleGroupEpisodes(value) {\n localStorageService.set(\"groupEpisodes\", value);\n $scope.shared.isGroupEpisodes = value;\n $scope.foo.isGroupEpisodes = value;\n blockAndUpdate();\n }\n\n function toggleExpandGroups(value) {\n localStorageService.set(\"expandGroupsByDefault\", value);\n $scope.shared.isExpandGroupsByDefault = value;\n $scope.foo.isExpandGroupsByDefault = value;\n blockAndUpdate();\n }\n\n function toggleDownloadedIndicator(value) {\n localStorageService.set(\"showDownloadedIndicator\", value);\n $scope.shared.showDownloadedIndicator = value;\n $scope.foo.showDownloadedIndicator = value;\n blockAndUpdate();\n }\n\n function toggleHideAlreadyDownloadedResults(value) {\n localStorageService.set(\"hideAlreadyDownloadedResults\", value);\n $scope.foo.hideAlreadyDownloadedResults = value;\n blockAndUpdate();\n }\n\n function toggleShowResultsAsZipButton(value) {\n localStorageService.set(\"showResultsAsZipButton\", value);\n $scope.shared.showResultsAsZipButton = value;\n $scope.foo.showResultsAsZipButton = value;\n }\n\n function toggleAlwaysShowTitles(value) {\n localStorageService.set(\"alwaysShowTitles\", value);\n $scope.shared.alwaysShowTitles = value;\n $scope.foo.alwaysShowTitles = value;\n $scope.$broadcast(\"toggleAlwaysShowTitles\", value);\n }\n\n\n $scope.indexersForFiltering = [];\n _.forEach($scope.indexersearches, function (indexer) {\n $scope.indexersForFiltering.push({label: indexer.indexerName, id: indexer.indexerName})\n });\n $scope.categoriesForFiltering = [];\n _.forEach(CategoriesService.getWithoutAll(), function (category) {\n $scope.categoriesForFiltering.push({label: category.name, id: category.name})\n });\n _.forEach($scope.indexersearches, function (ps) {\n $scope.indexerResultsInfo[ps.indexerName.toLowerCase()] = {loadedResults: ps.loaded_results};\n });\n\n setDataFromSearchResult(SearchService.getLastResults(), []);\n $scope.$emit(\"searchResultsShown\");\n\n if (!SearchService.getLastResults().searchResults || SearchService.getLastResults().searchResults.length === 0 || $scope.allResultsFiltered || $scope.numberOfAcceptedResults === 0) {\n //Close modal instance because no search results will be rendered that could trigger the closing\n console.log(\"CLosing status window\");\n SearchService.getModalInstance().close();\n $scope.doShowResults = true;\n } else {\n console.log(\"Will leave the closing of the status window to finishRendering. # of search results: \" + SearchService.getLastResults().searchResults.length + \". All results filtered: \" + $scope.allResultsFiltered);\n }\n\n //Returns the content of the property (defined by the current sortPredicate) of the first group element\n $scope.firstResultPredicate = firstResultPredicate;\n\n function firstResultPredicate(item) {\n return item[0][$scope.sortPredicate];\n }\n\n //Returns the unique group identifier which allows angular to keep track of the grouped search results even after filtering, making filtering by indexers a lot faster (albeit still somewhat slow...)\n $scope.groupId = groupId;\n\n function groupId(item) {\n return item[0][0].searchResultId;\n }\n\n $scope.onFilterButtonsModelChange = function () {\n console.log($scope.filterButtonsModel);\n blockAndUpdate();\n };\n\n function blockAndUpdate() {\n startBlocking(\"Sorting / filtering...\").then(function () {\n [$scope.filteredResults, $scope.filterReasons] = sortAndFilter(allSearchResults);\n localStorageService.set(\"sorting\", sortModel);\n });\n }\n\n //Block the UI and return after timeout. This way we make sure that the blocking is done before angular starts updating the model/view. There's probably a better way to achieve that?\n function startBlocking(message) {\n var deferred = $q.defer();\n blockUI.start(message);\n $timeout(function () {\n deferred.resolve();\n }, 10);\n return deferred.promise;\n }\n\n $scope.$on(\"sort\", function (event, column, sortMode, reversed) {\n if (sortMode === 0) {\n sortModel = {\n column: \"epoch\",\n sortMode: 2,\n reversed: true\n };\n } else {\n sortModel = {\n column: column,\n sortMode: sortMode,\n reversed: reversed\n };\n }\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode, sortModel.reversed);\n }, 10);\n blockAndUpdate();\n });\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filterValue && isActive) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n blockAndUpdate();\n });\n\n $scope.resort = function () {\n };\n\n function getCleanedTitle(element) {\n try {\n return element.title.toLowerCase().replace(/[\\s\\-\\._]/ig, \"\");\n } catch (e) {\n console.error(\"Unable to clean title for result \" + element);\n }\n }\n\n function getGroupingString(element) {\n\n var groupingString;\n if ($scope.shared.isGroupEpisodes) {\n groupingString = (element.showtitle + \"x\" + element.season + \"x\" + element.episode).toLowerCase().replace(/[\\._\\-]/ig, \"\");\n if (groupingString === \"nullxnullxnull\") {\n groupingString = getCleanedTitle(element);\n }\n } else {\n groupingString = getCleanedTitle(element);\n if (!$scope.foo.groupTorrentAndNewznabResults) {\n groupingString = groupingString + element.downloadType;\n }\n }\n return groupingString;\n }\n\n function sortAndFilter(results) {\n var query;\n var words;\n var filterReasons = {\n \"tooSmall\": 0,\n \"tooLarge\": 0,\n \"tooYoung\": 0,\n \"tooOld\": 0,\n \"tooFewGrabs\": 0,\n \"tooManyGrabs\": 0,\n \"title\": 0,\n \"tooindexer\": 0,\n \"category\": 0,\n \"tooOld\": 0,\n \"quickFilter\": 0,\n \"alreadyDownloaded\": 0\n\n\n };\n\n if (\"title\" in $scope.filterModel) {\n query = $scope.filterModel.title.filterValue;\n if (!(query.startsWith(\"/\") && query.endsWith(\"/\"))) {\n words = query.toLowerCase().split(/[\\s.\\-]+/);\n }\n }\n\n function filter(item) {\n if (item.title === null || item.title === undefined) {\n //https://github.com/theotherp/nzbhydra2/issues/690\n console.error(\"Item without title: \" + JSON.stringify(item))\n }\n if (\"size\" in $scope.filterModel) {\n var filterValue = $scope.filterModel.size.filterValue;\n if (angular.isDefined(filterValue.min) && item.size / 1024 / 1024 < filterValue.min) {\n filterReasons[\"tooSmall\"] = filterReasons[\"tooSmall\"] + 1;\n return false;\n }\n if (angular.isDefined(filterValue.max) && item.size / 1024 / 1024 > filterValue.max) {\n filterReasons[\"tooLarge\"] = filterReasons[\"tooLarge\"] + 1;\n return false;\n }\n }\n\n if (\"epoch\" in $scope.filterModel) {\n var filterValue = $scope.filterModel.epoch.filterValue;\n var ageDays = moment.utc().diff(moment.unix(item.epoch), \"days\");\n if (angular.isDefined(filterValue.min) && ageDays < filterValue.min) {\n filterReasons[\"tooYoung\"] = filterReasons[\"tooYoung\"] + 1;\n return false;\n }\n if (angular.isDefined(filterValue.max) && ageDays > filterValue.max) {\n filterReasons[\"tooOld\"] = filterReasons[\"tooOld\"] + 1;\n return false;\n }\n }\n\n if (\"grabs\" in $scope.filterModel) {\n var filterValue = $scope.filterModel.grabs.filterValue;\n if (angular.isDefined(filterValue.min)) {\n if ((item.seeders !== null && item.seeders < filterValue.min) || (item.seeders === null && item.grabs !== null && item.grabs < filterValue.min)) {\n filterReasons[\"tooFewGrabs\"] = filterReasons[\"tooFewGrabs\"] + 1;\n return false;\n }\n }\n if (angular.isDefined(filterValue.max)) {\n if ((item.seeders !== null && item.seeders > filterValue.max) || (item.seeders === null && item.grabs !== null && item.grabs > filterValue.max)) {\n filterReasons[\"tooManyGrabs\"] = filterReasons[\"tooManyGrabs\"] + 1;\n return false;\n }\n }\n }\n\n if (\"title\" in $scope.filterModel) {\n var ok;\n if (query.startsWith(\"/\") && query.endsWith(\"/\")) {\n ok = item.title.toLowerCase().match(new RegExp(query.substr(1, query.length - 2), \"gi\"));\n } else {\n ok = _.every(words, function (word) {\n if (word.startsWith(\"!\")) {\n if (word.length === 1) {\n return true;\n }\n return item.title.toLowerCase().indexOf(word.substring(1)) === -1;\n }\n return item.title.toLowerCase().indexOf(word) > -1;\n });\n }\n if (!ok) {\n filterReasons[\"title\"] = filterReasons[\"title\"] + 1;\n return false;\n }\n }\n if (\"indexer\" in $scope.filterModel) {\n if (_.indexOf($scope.filterModel.indexer.filterValue, item.indexer) === -1) {\n filterReasons[\"title\"] = filterReasons[\"title\"] + 1;\n return false;\n }\n }\n if (\"category\" in $scope.filterModel) {\n if (_.indexOf($scope.filterModel.category.filterValue, item.category) === -1) {\n filterReasons[\"category\"] = filterReasons[\"category\"] + 1;\n return false;\n }\n }\n if ($scope.filterButtonsModel.source !== null) {\n var mustContain = [];\n _.each($scope.filterButtonsModel.source, function (value, key) { //key is something like 'camts', value is true or false\n if (value) {\n Array.prototype.push.apply(mustContain, $scope.filterButtonsModelMap[key]);\n }\n });\n if (mustContain.length > 0) {\n var containsAtLeastOne = _.any(mustContain, function (word) {\n return item.title.toLowerCase().indexOf(word.toLowerCase()) > -1\n });\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the words \" + JSON.stringify(mustContain));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n }\n if ($scope.filterButtonsModel.quality !== null && !_.isEmpty($scope.filterButtonsModel.quality)) {\n //key is something like 'q720p', value is true or false.\n var requiresAnyOf = _.keys(_.pick($scope.filterButtonsModel.quality, function (value, key) {\n return value\n }));\n if (requiresAnyOf.length === 0) {\n return true;\n }\n\n var containsAtLeastOne = _.any(requiresAnyOf, function (required) {\n if (item.title.toLowerCase().indexOf(required.substring(1).toLowerCase()) > -1) {\n //We need to remove the \"q\" which is there because keys may not start with a digit\n return true;\n }\n })\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the qualities \" + JSON.stringify(requiresAnyOf));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n if ($scope.filterButtonsModel.other !== null && !_.isEmpty($scope.filterButtonsModel.other)) {\n var requiresAnyOf = _.keys(_.pick($scope.filterButtonsModel.other, function (value, key) {\n return value\n }));\n if (requiresAnyOf.length === 0) {\n return true;\n }\n var containsAtLeastOne = _.any(requiresAnyOf, function (required) {\n if (item.title.toLowerCase().indexOf(required.toLowerCase()) > -1) {\n return true;\n }\n })\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the 'other' values \" + JSON.stringify(requiresAnyOf));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n if ($scope.filterButtonsModel.custom !== null && !_.isEmpty($scope.filterButtonsModel.custom)) {\n var requiresAnyOf = _.keys(_.pick($scope.filterButtonsModel.custom, function (value, key) {\n return value\n }));\n if (requiresAnyOf.length === 0) {\n return true;\n }\n var containsAtLeastOne = _.any(requiresAnyOf, function (required) {\n if (item.title.toLowerCase().indexOf(required.toLowerCase()) > -1) {\n return true;\n }\n })\n if (!containsAtLeastOne) {\n console.debug(item.title + \" does not contain any of the custom values' \" + JSON.stringify(requiresAnyOf));\n filterReasons[\"quickFilter\"] = filterReasons[\"quickFilter\"] + 1;\n return false;\n }\n }\n\n if ($scope.foo.hideAlreadyDownloadedResults && item.downloadedAt !== null) {\n filterReasons[\"alreadyDownloaded\"] = filterReasons[\"alreadyDownloaded\"] + 1;\n return false;\n }\n\n return true;\n }\n\n\n var sortPredicateKey = sortModel.column;\n var sortReversed = sortModel.reversed;\n\n function getSortPredicateValue(containgObject) {\n var sortPredicateValue;\n if (sortPredicateKey === \"grabs\") {\n if (containgObject[\"seeders\"] !== null) {\n sortPredicateValue = containgObject[\"seeders\"];\n } else if (containgObject[\"grabs\"] !== null) {\n sortPredicateValue = containgObject[\"grabs\"];\n } else {\n sortPredicateValue = 0;\n }\n } else if (sortPredicateKey === \"title\") {\n sortPredicateValue = getCleanedTitle(containgObject);\n } else if (sortPredicateKey === \"indexer\") {\n sortPredicateValue = containgObject[\"indexer\"].toLowerCase();\n } else {\n sortPredicateValue = containgObject[sortPredicateKey];\n }\n return sortPredicateValue;\n }\n\n function createSortedHashgroups(titleGroup) {\n function createHashGroup(hashGroup) {\n //Sorting hash group's contents should not matter for size and age and title but might for category (we might remove this, it's probably mostly unnecessary)\n var sortedHashGroup = _.sortBy(hashGroup, function (item) {\n var sortPredicateValue = getSortPredicateValue(item);\n return sortReversed ? -sortPredicateValue : sortPredicateValue;\n });\n //Now sort the hash group by indexer score (inverted) so that the result with the highest indexer score is shown on top (or as the only one of a hash group if it's collapsed)\n sortedHashGroup = _.sortBy(sortedHashGroup, function (item) {\n return item.indexerscore * -1;\n });\n return sortedHashGroup;\n }\n\n function getHashGroupFirstElementSortPredicate(hashGroup) {\n if (sortPredicateKey === \"title\") {\n //Sorting a title group internally by title doesn't make sense so fall back to sorting by age so that newest result is at the top\n return ((10000000000 * hashGroup[0][\"indexerscore\"]) + hashGroup[0][\"epoch\"]) * -1;\n }\n return getSortPredicateValue(hashGroup[0]);\n }\n\n var grouped = _.groupBy(titleGroup, \"hash\");\n var mapped = _.map(grouped, createHashGroup);\n var sorted = _.sortBy(mapped, getHashGroupFirstElementSortPredicate);\n if (sortModel.sortMode === 2 && sortPredicateKey !== \"title\") {\n sorted = sorted.reverse();\n }\n\n return sorted;\n }\n\n function getTitleGroupFirstElementsSortPredicate(titleGroup) {\n var sortPredicateValue;\n if (sortPredicateKey === \"grabs\" && $scope.foo.sumGrabs) {\n var sumOfGrabs = 0;\n _.each(titleGroup, function (element1) {\n _.each(element1, function (element2) {\n sumOfGrabs += getSortPredicateValue(element2);\n })\n });\n\n sortPredicateValue = sumOfGrabs;\n } else {\n sortPredicateValue = getSortPredicateValue(titleGroup[0][0]);\n }\n return sortPredicateValue\n }\n\n _.each(results, function (result) {\n var indexerColor = indexerColors[result.indexer];\n if (indexerColor === undefined || indexerColor === null) {\n return \"\";\n }\n result.style = \"background-color: \" + indexerColor.replace(\"rgb\", \"rgba\").replace(\")\", \",0.5)\")\n });\n\n var filtered = _.filter(results, filter);\n $scope.numberOfFilteredResults = results.length - filtered.length;\n $scope.allResultsFiltered = results.length > 0 && ($scope.numberOfFilteredResults === results.length);\n console.log(\"Filtered \" + $scope.numberOfFilteredResults + \" out of \" + results.length);\n var newSelected = $scope.selected;\n _.forEach($scope.selected, function (x) {\n if (x === undefined) {\n return;\n }\n if (filtered.indexOf(x) === -1) {\n $scope.$broadcast(\"toggleSelection\", x, false);\n newSelected.splice($scope.selected.indexOf(x), 1);\n }\n });\n $scope.selected = newSelected;\n\n var grouped = _.groupBy(filtered, getGroupingString);\n\n var mapped = _.map(grouped, createSortedHashgroups);\n var sorted = _.sortBy(mapped, getTitleGroupFirstElementsSortPredicate);\n if (sortModel.sortMode === 2) {\n sorted = sorted.reverse();\n }\n\n $scope.lastClickedRowIndex = null;\n\n var filteredResults = [];\n var countTitleGroups = 0;\n var countResultsUntilTitleGroupLimitReached = 0;\n _.forEach(sorted, function (titleGroup) {\n var titleGroupIndex = 0;\n countTitleGroups++;\n\n _.forEach(titleGroup, function (duplicateGroup) {\n var duplicateIndex = 0;\n _.forEach(duplicateGroup, function (result) {\n try {\n result.titleGroupIndicator = getGroupingString(result);\n result.titleGroupIndex = titleGroupIndex;\n result.duplicateGroupIndex = duplicateIndex;\n result.duplicatesLength = duplicateGroup.length;\n result.titlesLength = titleGroup.length;\n filteredResults.push(result);\n duplicateIndex += 1;\n if (countTitleGroups <= $scope.limitTo) {\n countResultsUntilTitleGroupLimitReached++;\n }\n if (duplicateGroup.length > 1)\n $scope.countDuplicates += (duplicateGroup.length - 1)\n } catch (e) {\n console.error(\"Error while processing result \" + result, e);\n }\n });\n titleGroupIndex += 1;\n });\n });\n $scope.limitTo = Math.max($scope.limitTo, countResultsUntilTitleGroupLimitReached);\n\n $scope.$broadcast(\"calculateDisplayState\");\n\n return [filteredResults, filterReasons];\n }\n\n $scope.toggleTitlegroupExpand = function toggleTitlegroupExpand(titleGroup) {\n $scope.groupExpanded[titleGroup[0][0].title] = !$scope.groupExpanded[titleGroup[0][0].title];\n $scope.groupExpanded[titleGroup[0][0].hash] = !$scope.groupExpanded[titleGroup[0][0].hash];\n };\n\n $scope.stopBlocking = stopBlocking;\n\n function stopBlocking() {\n blockUI.reset();\n }\n\n function setDataFromSearchResult(data, previousSearchResults) {\n allSearchResults = previousSearchResults.concat(data.searchResults);\n allSearchResults = uniq(allSearchResults);\n [$scope.filteredResults, $scope.filterReasons] = sortAndFilter(allSearchResults);\n\n $scope.numberOfAvailableResults = data.numberOfAvailableResults;\n $scope.rejectedReasonsMap = data.rejectedReasonsMap;\n $scope.anyResultsRejected = !_.isEmpty(data.rejectedReasonsMap);\n $scope.anyIndexersSearchedSuccessfully = _.any(data.indexerSearchMetaDatas, function (x) {\n return x.wasSuccessful;\n });\n $scope.numberOfAcceptedResults = data.numberOfAcceptedResults;\n $scope.numberOfRejectedResults = data.numberOfRejectedResults;\n $scope.numberOfProcessedResults = data.numberOfProcessedResults;\n $scope.numberOfDuplicateResults = data.numberOfDuplicateResults;\n $scope.numberOfLoadedResults = allSearchResults.length;\n $scope.indexersearches = data.indexerSearchMetaDatas;\n\n $scope.loadMoreEnabled = ($scope.numberOfLoadedResults + $scope.numberOfRejectedResults < $scope.numberOfAvailableResults) || _.any(data.indexerSearchMetaDatas, function (x) {\n return x.hasMoreResults;\n });\n $scope.totalAvailableUnknown = _.any(data.indexerSearchMetaDatas, function (x) {\n return !x.totalResultsKnown;\n });\n\n if (!$scope.foo.indexerStatusesExpanded && _.any(data.indexerSearchMetaDatas, function (x) {\n return !x.wasSuccessful;\n })) {\n growl.info(\"Errors occurred during searching, Check indexer statuses\")\n }\n //Only show those categories in filter that are actually present in the results\n $scope.categoriesForFiltering = [];\n var allUsedCategories = _.uniq(_.pluck(allSearchResults, \"category\"));\n _.forEach(CategoriesService.getWithoutAll(), function (category) {\n if (allUsedCategories.indexOf(category.name) > -1) {\n $scope.categoriesForFiltering.push({label: category.name, id: category.name})\n }\n });\n }\n\n function uniq(searchResults) {\n var seen = {};\n var out = [];\n var len = searchResults.length;\n var j = 0;\n for (var i = 0; i < len; i++) {\n var item = searchResults[i];\n if (seen[item.searchResultId] !== 1) {\n seen[item.searchResultId] = 1;\n out[j++] = item;\n }\n }\n return out;\n }\n\n $scope.loadMore = loadMore;\n\n function loadMore(loadAll) {\n startBlocking(loadAll ? \"Loading all results...\" : \"Loading more results...\").then(function () {\n $scope.loadingMore = true;\n var limit = loadAll ? $scope.numberOfAvailableResults - $scope.numberOfProcessedResults : null;\n SearchService.loadMore($scope.numberOfLoadedResults, limit, loadAll).then(function (data) {\n setDataFromSearchResult(data, allSearchResults);\n $scope.loadingMore = false;\n //stopBlocking();\n });\n });\n }\n\n\n $scope.countResults = countResults;\n\n function countResults() {\n return allSearchResults.length;\n }\n\n $scope.invertSelection = function invertSelection() {\n $scope.$broadcast(\"invertSelection\");\n };\n\n $scope.deselectAll = function deselectAll() {\n $scope.$broadcast(\"deselectAll\");\n };\n\n $scope.selectAll = function selectAll() {\n $scope.$broadcast(\"selectAll\");\n };\n\n $scope.toggleIndexerStatuses = function () {\n $scope.foo.indexerStatusesExpanded = !$scope.foo.indexerStatusesExpanded;\n localStorageService.set(\"indexerStatusesExpanded\", $scope.foo.indexerStatusesExpanded);\n };\n\n $scope.getRejectedReasonsTooltip = function () {\n if (_.isEmpty($scope.rejectedReasonsMap)) {\n return \"No rejected results\";\n } else {\n var tooltip = \"Rejected results:
              \";\n tooltip += '';\n _.forEach($scope.rejectedReasonsMap, function (count, reason) {\n tooltip += '';\n });\n tooltip += '
              CountReason
              ' + count + '' + reason + '
              ';\n tooltip += '
              ';\n tooltip += \"Filtered results:
              \";\n tooltip += '';\n _.forEach($scope.filterReasons, function (count, reason) {\n if (count > 0) {\n tooltip += '';\n }\n });\n tooltip += '
              CountReason
              ' + count + '' + reason + '
              ';\n tooltip += '
              '\n return tooltip;\n }\n };\n\n\n $scope.$on(\"checkboxClicked\", function (event, originalEvent, rowIndex, newCheckedValue, clickTargetElement) {\n if (originalEvent.shiftKey && $scope.lastClickedRowIndex !== null) {\n $scope.$broadcast(\"shiftClick\", Number($scope.lastClickedRowIndex), Number(rowIndex), Number($scope.lastClickedValue), $scope.lastClickedElement, clickTargetElement);\n }\n $scope.lastClickedRowIndex = rowIndex;\n $scope.lastClickedElement = clickTargetElement;\n $scope.lastClickedValue = newCheckedValue;\n });\n\n $scope.$on(\"toggleTitleExpansionUp\", function ($event, value, titleGroupIndicator) {\n $scope.$broadcast(\"toggleTitleExpansionDown\", value, titleGroupIndicator);\n });\n\n $scope.$on(\"toggleDuplicateExpansionUp\", function ($event, value, hash) {\n $scope.$broadcast(\"toggleDuplicateExpansionDown\", value, hash);\n });\n\n $scope.$on(\"selectionUp\", function ($event, result, value) {\n var index = $scope.selected.indexOf(result);\n if (value && index === -1) {\n $scope.selected.push(result);\n } else if (!value && index > -1) {\n $scope.selected.splice(index, 1);\n }\n });\n\n $scope.downloadNzbsCallback = function (addedIds) {\n if (addedIds !== null && addedIds.length > 0) {\n growl.info(\"Removing downloaded NZBs from selection\");\n var toRemove = _.filter($scope.selected, function (x) {\n return addedIds.indexOf(Number(x.searchResultId)) > -1;\n });\n var newSelected = $scope.selected;\n _.forEach(toRemove, function (x) {\n $scope.$broadcast(\"toggleSelection\", x, false);\n newSelected.splice($scope.selected.indexOf(x), 1);\n });\n $scope.selected = newSelected;\n }\n };\n\n\n $scope.filterRejectedZero = function () {\n return function (entry) {\n return entry[1] > 0;\n }\n };\n\n $scope.onPageChange = function (newPageNumber, oldPageNumber) {\n _.each($scope.selected, function (x) {\n $scope.$broadcast(\"toggleSelection\", x, true);\n })\n };\n\n $scope.$on(\"onFinishRender\", function () {\n console.log(\"Finished rendering results.\")\n $scope.doShowResults = true;\n $timeout(function () {\n if ($scope.foo.scrollToResults) {\n var searchResultsElement = angular.element(document.getElementById('display-options'));\n $document.scrollToElement(searchResultsElement, 0, 500);\n }\n stopBlocking();\n console.log(\"Closing search status window because rendering is finished.\")\n SearchService.getModalInstance().close();\n }, 1);\n });\n\n\n $timeout(function () {\n DebugService.print();\n }, 3000);\n\n // $timeout(function () {\n // function getWatchers(root) {\n // root = angular.element(root || document.documentElement);\n // var watcherCount = 0;\n // var ids = [];\n //\n // function getElemWatchers(element, ids) {\n // var isolateWatchers = getWatchersFromScope(element.data().$isolateScope, ids);\n // var scopeWatchers = getWatchersFromScope(element.data().$scope, ids);\n // var watchers = scopeWatchers.concat(isolateWatchers);\n // angular.forEach(element.children(), function (childElement) {\n // watchers = watchers.concat(getElemWatchers(angular.element(childElement), ids));\n // });\n // return watchers;\n // }\n //\n // function getWatchersFromScope(scope, ids) {\n // if (scope) {\n // if (_.indexOf(ids, scope.$id) > -1) {\n // return [];\n // }\n // ids.push(scope.$id);\n // if (scope.$$watchers) {\n // if (scope.$$watchers.length > 1) {\n // var a;\n // a = 1;\n // }\n // return scope.$$watchers;\n // }\n // {\n // return [];\n // }\n //\n // } else {\n // return [];\n // }\n // }\n //\n // return getElemWatchers(root, ids);\n // }\n //\n // }, $scope.limitTo);\n}\n","\r\nSearchHistoryService.$inject = [\"$filter\", \"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('SearchHistoryService', SearchHistoryService);\r\n\r\nfunction SearchHistoryService($filter, $http) {\r\n\r\n return {\r\n getSearchHistory: getSearchHistory,\r\n getSearchHistoryForSearching: getSearchHistoryForSearching,\r\n formatRequest: formatRequest,\r\n getStateParamsForRepeatedSearch: getStateParamsForRepeatedSearch\r\n };\r\n\r\n function getSearchHistoryForSearching() {\r\n return $http.post(\"internalapi/history/searches/forsearching\").then(function (response) {\r\n return {\r\n searchRequests: response.data\r\n }\r\n });\r\n }\r\n\r\n function getSearchHistory(pageNumber, limit, filterModel, sortModel, distinct, onlyCurrentUser) {\r\n var params = {\r\n page: pageNumber,\r\n limit: limit,\r\n filterModel: filterModel,\r\n distinct: distinct,\r\n onlyCurrentUser: onlyCurrentUser\r\n };\r\n if (angular.isUndefined(pageNumber)) {\r\n params.page = 1;\r\n }\r\n if (angular.isUndefined(limit)) {\r\n params.limit = 100;\r\n }\r\n if (angular.isUndefined(filterModel)) {\r\n params.filterModel = {}\r\n }\r\n if (!angular.isUndefined(sortModel)) {\r\n params.sortModel = sortModel;\r\n } else {\r\n params.sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n }\r\n return $http.post(\"internalapi/history/searches\", params).then(function (response) {\r\n return {\r\n searchRequests: response.data.content,\r\n totalRequests: response.data.totalElements\r\n }\r\n });\r\n }\r\n\r\n function formatRequest(request, includeIdLink, includequery, describeEmptySearch, includeTitle) {\r\n var result = [];\r\n result.push('Category: ' + request.categoryName);\r\n if (includequery && request.query) {\r\n result.push('Query: ' + request.query);\r\n }\r\n if (request.title && includeTitle) {\r\n result.push('Title: ' + request.title);\r\n } //Only include identifiers if title is unknown\r\n else if (request.identifiers.length > 0) {\r\n var href;\r\n var key;\r\n var value;\r\n var identifiers = _.indexBy(request.identifiers, 'identifierKey');\r\n if (\"IMDB\" in identifiers) {\r\n key = \"IMDB ID\";\r\n value = identifiers.IMDB.identifierValue;\r\n href = \"https://www.imdb.com/title/tt\" + value;\r\n } else if (\"TVDB\" in identifiers) {\r\n key = \"TVDB ID\";\r\n value = identifiers.TVDB.identifierValue;\r\n href = \"https://thetvdb.com/?tab=series&id=\" + value;\r\n } else if (\"TVRAGE\" in identifiers) {\r\n key = \"TVRage ID\";\r\n value = identifiers.TVRAGE.identifierValue;\r\n href = \"internalapi/redirect_rid?rid=\" + value;\r\n } else if (\"TMDB\" in identifiers) {\r\n key = \"TMDB ID\";\r\n value = identifiers.TMDB.identifierValue;\r\n href = \"https://www.themoviedb.org/movie/\" + value;\r\n }\r\n href = $filter(\"dereferer\")(href);\r\n if (includeIdLink) {\r\n result.push('' + key + ': ' + value + \"\");\r\n } else {\r\n result.push('' + key + \": \" + value);\r\n }\r\n }\r\n if (request.season) {\r\n result.push('Season: ' + request.season);\r\n }\r\n if (request.episode) {\r\n result.push('Episode: ' + request.episode);\r\n }\r\n if (request.author) {\r\n result.push('Author: ' + request.author);\r\n }\r\n if (result.length === 0 && describeEmptySearch) {\r\n result = ['Empty search'];\r\n }\r\n\r\n return result.join(\", \");\r\n\r\n }\r\n\r\n function getStateParamsForRepeatedSearch(request) {\r\n var stateParams = {};\r\n stateParams.mode = \"search\";\r\n var availableIdentifiers = _.pluck(request.identifiers, \"identifierKey\");\r\n if (availableIdentifiers.indexOf(\"TMDB\") > -1 || availableIdentifiers.indexOf(\"IMDB\") > -1) {\r\n stateParams.mode = \"movie\";\r\n } else if (availableIdentifiers.indexOf(\"TVRAGE\") > -1 || availableIdentifiers.indexOf(\"TVMAZE\") > -1 || availableIdentifiers.indexOf(\"TVDB\") > -1) {\r\n stateParams.mode = \"tvsearch\";\r\n }\r\n if (request.season) {\r\n stateParams.season = request.season;\r\n }\r\n if (request.episode) {\r\n stateParams.episode = request.episode;\r\n }\r\n\r\n _.each(request.identifiers, function (entry) {\r\n switch (entry.identifierKey) {\r\n case \"TMDB\":\r\n stateParams.tmdbId = entry.identifierValue;\r\n break;\r\n case \"IMDB\":\r\n stateParams.imdbId = entry.identifierValue;\r\n break;\r\n case \"TVMAZE\":\r\n stateParams.tvmazeId = entry.identifierValue;\r\n break;\r\n case \"TVRAGE\":\r\n stateParams.tvrageId = entry.identifierValue;\r\n break;\r\n case \"TVDB\":\r\n stateParams.tvdbId = entry.identifierValue;\r\n break;\r\n }\r\n });\r\n\r\n\r\n if (request.query !== \"\") {\r\n stateParams.query = request.query;\r\n }\r\n\r\n if (request.title) {\r\n stateParams.title = request.title;\r\n }\r\n\r\n if (request.categoryName) {\r\n stateParams.category = request.categoryName;\r\n }\r\n\r\n return stateParams;\r\n }\r\n\r\n\r\n}","\r\nSearchHistoryController.$inject = [\"$scope\", \"$state\", \"SearchHistoryService\", \"ConfigService\", \"history\", \"$sce\", \"$filter\", \"$timeout\", \"$http\", \"$uibModal\"];angular\r\n .module('nzbhydraApp')\r\n .controller('SearchHistoryController', SearchHistoryController);\r\n\r\n\r\nfunction SearchHistoryController($scope, $state, SearchHistoryService, ConfigService, history, $sce, $filter, $timeout, $http, $uibModal) {\r\n $scope.limit = 100;\r\n $scope.pagination = {\r\n current: 1\r\n };\r\n var sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n $timeout(function () {\r\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\r\n }, 10);\r\n $scope.filterModel = {};\r\n\r\n //Filter options\r\n $scope.categoriesForFiltering = [];\r\n _.forEach(ConfigService.getSafe().categoriesConfig.categories, function (category) {\r\n $scope.categoriesForFiltering.push({label: category.name, id: category.name})\r\n });\r\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\r\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: 'API'}, {\r\n label: \"Internal\",\r\n value: 'INTERNAL'\r\n }];\r\n\r\n //Preloaded data\r\n $scope.searchRequests = history.searchRequests;\r\n $scope.totalRequests = history.totalRequests;\r\n\r\n var anyUsername = false;\r\n var anyIp = false;\r\n for (var request in $scope.searchRequests) {\r\n if (request.username) {\r\n anyUsername = true;\r\n }\r\n if (request.ip) {\r\n anyIp = true;\r\n }\r\n if (anyIp && anyUsername) {\r\n break;\r\n }\r\n }\r\n $scope.columnSizes = {\r\n time: 10,\r\n query: 30,\r\n category: 10,\r\n additionalParameters: 22,\r\n source: 8,\r\n username: 10,\r\n ip: 10\r\n };\r\n if (ConfigService.getSafe().logging.historyUserInfoType === \"NONE\" || (!anyUsername && !anyIp)) {\r\n $scope.columnSizes.username = 0;\r\n $scope.columnSizes.ip = 0;\r\n $scope.columnSizes.query += 10;\r\n $scope.columnSizes.additionalParameters += 10;\r\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"IP\") {\r\n $scope.columnSizes.username = 0;\r\n $scope.columnSizes.query += 5;\r\n $scope.columnSizes.additionalParameters += 5;\r\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"USERNAME\") {\r\n $scope.columnSizes.ip = 0;\r\n $scope.columnSizes.query += 5;\r\n $scope.columnSizes.additionalParameters += 5;\r\n }\r\n\r\n $scope.update = function () {\r\n SearchHistoryService.getSearchHistory($scope.pagination.current, $scope.limit, $scope.filterModel, sortModel).then(function (history) {\r\n $scope.searchRequests = history.searchRequests;\r\n $scope.totalRequests = history.totalRequests;\r\n });\r\n };\r\n\r\n $scope.$on(\"sort\", function (event, column, sortMode) {\r\n if (sortMode === 0) {\r\n sortModel = {\r\n column: \"time\",\r\n sortMode: 2\r\n };\r\n } else {\r\n sortModel = {\r\n column: column,\r\n sortMode: sortMode\r\n };\r\n }\r\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\r\n $scope.update();\r\n });\r\n\r\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\r\n if (filterModel.filterValue) {\r\n $scope.filterModel[column] = filterModel;\r\n } else {\r\n delete $scope.filterModel[column];\r\n }\r\n $scope.update();\r\n });\r\n\r\n\r\n $scope.openSearch = function (request) {\r\n $state.go(\"root.search\", SearchHistoryService.getStateParamsForRepeatedSearch(request), {\r\n inherit: false,\r\n notify: true,\r\n reload: true\r\n });\r\n };\r\n\r\n $scope.formatQuery = function (request) {\r\n if (request.title) {\r\n return request.title;\r\n }\r\n\r\n if (!request.query && request.identifiers.length === 0 && !request.season && !request.episode) {\r\n return \"Update query\";\r\n }\r\n return request.query;\r\n };\r\n\r\n $scope.formatAdditional = function (request) {\r\n var result = [];\r\n if (request.identifiers.length > 0) {\r\n var href;\r\n var key;\r\n var value;\r\n var pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"TMDB\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"TMDB ID\";\r\n href = \"https://www.themoviedb.org/movie/\" + pair.identifierValue;\r\n href = $filter(\"dereferer\")(href);\r\n value = pair.identifierValue;\r\n }\r\n\r\n pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"IMDB\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"IMDB ID\";\r\n href = (\"https://www.imdb.com/title/tt\" + pair.identifierValue).replace(\"tttt\", \"tt\");\r\n href = $filter(\"dereferer\")(href);\r\n value = pair.identifierValue;\r\n }\r\n\r\n pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"TVDB\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"TVDB ID\";\r\n href = \"https://thetvdb.com/?tab=series&id=\" + pair.identifierValue;\r\n href = $filter(\"dereferer\")(href);\r\n value = pair.identifierValue;\r\n }\r\n\r\n pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"TVMAZE\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"TVMAZE ID\";\r\n href = \"https://www.tvmaze.com/shows/\" + pair.identifierValue;\r\n href = $filter(\"dereferer\")(href);\r\n value = pair.identifierValue;\r\n }\r\n\r\n pair = _.find(request.identifiers, function (pair) {\r\n return pair.identifierKey === \"TVRAGE\"\r\n });\r\n if (angular.isDefined(pair)) {\r\n key = \"TVRage ID\";\r\n href = \"internalapi/redirectRid/\" + pair.identifierValue;\r\n value = pair.identifierValue;\r\n }\r\n\r\n result.push(key + \": \" + '' + value + \"\");\r\n }\r\n if (request.season) {\r\n result.push(\"Season: \" + request.season);\r\n }\r\n if (request.episode) {\r\n result.push(\"Episode: \" + request.episode);\r\n }\r\n if (request.author) {\r\n result.push(\"Author: \" + request.author);\r\n }\r\n return $sce.trustAsHtml(result.join(\", \"));\r\n };\r\n\r\n $scope.showDetails = function (searchId) {\r\n\r\n ModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$http\", \"searchId\"];\r\n function ModalInstanceCtrl($scope, $uibModalInstance, $http, searchId) {\r\n $http.get(\"internalapi/history/searches/details/\" + searchId).then(function (response) {\r\n $scope.details = response.data;\r\n });\r\n }\r\n\r\n $uibModal.open({\r\n templateUrl: 'static/html/search-history-details-modal.html',\r\n controller: ModalInstanceCtrl,\r\n size: \"md\",\r\n resolve: {\r\n searchId: function () {\r\n return searchId;\r\n }\r\n }\r\n });\r\n\r\n\r\n }\r\n\r\n}\r\n\r\n","\r\nSearchController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\", \"$uibModal\", \"$timeout\", \"$sce\", \"growl\", \"SearchService\", \"focus\", \"ConfigService\", \"HydraAuthService\", \"CategoriesService\", \"$element\", \"SearchHistoryService\"];\r\nSearchUpdateModalInstanceCtrl.$inject = [\"$scope\", \"$interval\", \"SearchService\", \"$uibModalInstance\", \"searchRequestId\", \"onCancel\", \"bootstrapped\"];angular\r\n .module('nzbhydraApp')\r\n .controller('SearchController', SearchController);\r\n\r\nfunction SearchController($scope, $http, $stateParams, $state, $uibModal, $timeout, $sce, growl, SearchService, focus, ConfigService, HydraAuthService, CategoriesService, $element, SearchHistoryService) {\r\n\r\n function getNumberOrUndefined(number) {\r\n if (_.isUndefined(number) || _.isNaN(number) || number === \"\") {\r\n return undefined;\r\n }\r\n number = parseInt(number);\r\n if (_.isNumber(number)) {\r\n return number;\r\n } else {\r\n return undefined;\r\n }\r\n }\r\n\r\n var searchRequestId = 0;\r\n var isSearchCancelled = false;\r\n var epochEnter;\r\n\r\n //Fill the form with the search values we got from the state params (so that their values are the same as in the current url)\r\n $scope.mode = $stateParams.mode;\r\n $scope.query = \"\";\r\n $scope.selectedItem = null;\r\n $scope.categories = _.filter(CategoriesService.getAllCategories(), function (c) {\r\n return c.mayBeSelected && !(c.ignoreResultsFrom === \"INTERNAL\" || c.ignoreResultsFrom === \"BOTH\");\r\n });\r\n $scope.minsize = getNumberOrUndefined($stateParams.minsize);\r\n $scope.maxsize = getNumberOrUndefined($stateParams.maxsize);\r\n if (angular.isDefined($stateParams.category) && $stateParams.category) {\r\n $scope.category = CategoriesService.getByName($stateParams.category);\r\n } else {\r\n $scope.category = CategoriesService.getDefault();\r\n $scope.minsize = $scope.category.minSizePreset;\r\n $scope.maxsize = $scope.category.maxSizePreset;\r\n }\r\n $scope.category = _.isNullOrEmpty($stateParams.category) ? CategoriesService.getDefault() : CategoriesService.getByName($stateParams.category);\r\n $scope.season = $stateParams.season;\r\n $scope.episode = $stateParams.episode;\r\n $scope.query = $stateParams.query;\r\n\r\n $scope.minage = getNumberOrUndefined($stateParams.minage);\r\n $scope.maxage = getNumberOrUndefined($stateParams.maxage);\r\n if (angular.isDefined($stateParams.indexers)) {\r\n $scope.indexers = decodeURIComponent($stateParams.indexers).split(\",\");\r\n }\r\n if (angular.isDefined($stateParams.title) || (angular.isDefined($stateParams.tmdbId) || angular.isDefined($stateParams.imdbId) || angular.isDefined($stateParams.tvmazeId) || angular.isDefined($stateParams.rid) || angular.isDefined($stateParams.tvdbId))) {\r\n var width = calculateWidth($stateParams.title) + 30;\r\n $scope.selectedItemWidth = width + \"px\";\r\n $scope.selectedItem = {\r\n tmdbId: $stateParams.tmdbId,\r\n imdbId: $stateParams.imdbId,\r\n tvmazeId: $stateParams.tvmazeId,\r\n rid: $stateParams.rid,\r\n tvdbId: $stateParams.tvdbId,\r\n title: $stateParams.title\r\n }\r\n }\r\n\r\n $scope.showIndexers = {};\r\n\r\n $scope.searchHistory = [];\r\n\r\n var safeConfig = ConfigService.getSafe();\r\n $scope.showIndexerSelection = HydraAuthService.getUserInfos().showIndexerSelection;\r\n\r\n\r\n $scope.typeAheadWait = 300;\r\n\r\n $scope.autocompleteLoading = false;\r\n $scope.isAskById = $scope.category.searchType === \"TVSEARCH\" || $scope.category.searchType === \"MOVIE\";\r\n $scope.isById = {value: $scope.selectedItem !== null || angular.isUndefined($scope.mode) || $scope.mode === null}; //If true the user wants to search by id so we enable autosearch. Was unable to achieve this using a simple boolean. Set to false if last search was not by ID\r\n $scope.availableIndexers = [];\r\n $scope.selectedIndexers = [];\r\n $scope.autocompleteClass = \"autocompletePosterMovies\";\r\n\r\n $scope.toggleCategory = function (searchCategory) {\r\n var oldCategory = $scope.category;\r\n $scope.category = searchCategory;\r\n\r\n //Show checkbox to ask if the user wants to search by ID (using autocomplete)\r\n if ($scope.category.searchType === \"TVSEARCH\" || $scope.category.searchType === \"MOVIE\") {\r\n $scope.isAskById = true;\r\n $scope.isById.value = true;\r\n } else {\r\n $scope.isAskById = false;\r\n $scope.isById.value = false;\r\n }\r\n\r\n if (oldCategory.searchType !== searchCategory.searchType) {\r\n $scope.selectedItem = null;\r\n }\r\n\r\n focus('searchfield');\r\n\r\n //Hacky way of triggering the autocomplete loading\r\n var searchModel = $element.find(\"#searchfield\").controller(\"ngModel\");\r\n if (angular.isDefined(searchModel.$viewValue)) {\r\n searchModel.$setViewValue(searchModel.$viewValue + \" \");\r\n }\r\n\r\n if (safeConfig.categoriesConfig.enableCategorySizes) {\r\n var min = searchCategory.minSizePreset;\r\n var max = searchCategory.maxSizePreset;\r\n if (_.isNumber(min)) {\r\n $scope.minsize = min;\r\n } else {\r\n $scope.minsize = \"\";\r\n }\r\n if (_.isNumber(max)) {\r\n $scope.maxsize = max;\r\n } else {\r\n $scope.maxsize = \"\";\r\n }\r\n }\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n };\r\n\r\n // Any function returning a promise object can be used to load values asynchronously\r\n $scope.getAutocomplete = function (val) {\r\n $scope.autocompleteLoading = true;\r\n //Expected model returned from API:\r\n //label: What to show in the results\r\n //title: Will be used for file search\r\n //value: Will be used as extraInfo (ttid oder tvdb id)\r\n //poster: url of poster to show\r\n\r\n //Don't use autocomplete if checkbox is disabled\r\n if (!$scope.isById.value || $scope.selectedItem) {\r\n return {};\r\n }\r\n\r\n if ($scope.category.searchType === \"MOVIE\") {\r\n return $http.get('internalapi/autocomplete/MOVIE', {params: {input: val}}).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data;\r\n });\r\n } else if ($scope.category.searchType === \"TVSEARCH\") {\r\n return $http.get('internalapi/autocomplete/TV', {params: {input: val}}).then(function (response) {\r\n $scope.autocompleteLoading = false;\r\n return response.data;\r\n });\r\n } else {\r\n return {};\r\n }\r\n };\r\n\r\n $scope.onTypeAheadEnter = function () {\r\n if (angular.isDefined(epochEnter)) {\r\n //Very hacky way of preventing a press of \"enter\" to select an autocomplete item from triggering a search\r\n //This is called *after* selectAutoComplete() is called\r\n var epochEnterNow = (new Date).getTime();\r\n var diff = epochEnterNow - epochEnter;\r\n if (diff > 50) {\r\n $scope.initiateSearch();\r\n }\r\n } else {\r\n $scope.initiateSearch();\r\n }\r\n };\r\n\r\n $scope.onTypeAheadKeyDown = function (event) {\r\n if (event.keyCode === 8) {\r\n if ($scope.query === \"\") {\r\n $scope.clearAutocomplete();\r\n }\r\n }\r\n };\r\n\r\n $scope.onDropOnQueryInput = function (event) {\r\n if ($scope.searchHistoryDragged === null || $scope.searchHistoryDragged === undefined) {\r\n return;\r\n }\r\n\r\n $scope.category = CategoriesService.getByName($scope.searchHistoryDragged.categoryName);\r\n $scope.season = $scope.searchHistoryDragged.season;\r\n $scope.episode = $scope.searchHistoryDragged.episode;\r\n $scope.query = $scope.searchHistoryDragged.query;\r\n\r\n if ($scope.searchHistoryDragged.title != null) {\r\n var width = calculateWidth($scope.searchHistoryDragged.title) + 30;\r\n $scope.selectedItemWidth = width + \"px\";\r\n }\r\n\r\n var tvmaze = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"TVMAZE\"});\r\n var tmdb = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"TMDB\"});\r\n var imdb = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"IMDB\"});\r\n var tvdb = _.findWhere($scope.searchHistoryDragged.identifiers, {identifierKey: \"TVDB\"});\r\n $scope.selectedItem = {\r\n tmdbId: tmdb === undefined ? null : tmdb.identifierValue,\r\n imdbId: imdb === undefined ? null : imdb.identifierValue,\r\n tvmazeId: tvmaze === undefined ? null : tvmaze.identifierValue,\r\n tvdbId: tvdb === undefined ? null : tvdb.identifierValue,\r\n title: $scope.searchHistoryDragged.title\r\n }\r\n\r\n event.preventDefault();\r\n\r\n $scope.searchHistoryDragged = null;\r\n focus('searchfield');\r\n $scope.status.isopen = false;\r\n }\r\n\r\n $scope.$on(\"searchHistoryDrag\", function (event, data) {\r\n $scope.searchHistoryDragged = JSON.parse(data);\r\n })\r\n\r\n //Is called when the search page is opened with params, either because the user initiated the search (which triggered a goTo to this page) or because a search URL was entered\r\n $scope.startSearch = function () {\r\n isSearchCancelled = false;\r\n searchRequestId = Math.round(Math.random() * 99999);\r\n var modalInstance = $scope.openModal(searchRequestId);\r\n\r\n var indexers = angular.isUndefined($scope.indexers) ? undefined : $scope.indexers.join(\",\");\r\n SearchService.search(searchRequestId, $scope.category.name, $scope.query, $scope.selectedItem, $scope.season, $scope.episode, $scope.minsize, $scope.maxsize, $scope.minage, $scope.maxage, indexers, $scope.mode).then(function () {\r\n //modalInstance.close();\r\n SearchService.setModalInstance(modalInstance);\r\n if (!isSearchCancelled) {\r\n $state.go(\"root.search.results\", {\r\n minsize: $scope.minsize,\r\n maxsize: $scope.maxsize,\r\n minage: $scope.minage,\r\n maxage: $scope.maxage\r\n }, {\r\n inherit: true\r\n });\r\n }\r\n },\r\n function () {\r\n modalInstance.close();\r\n });\r\n };\r\n\r\n $scope.openModal = function openModal(searchRequestId) {\r\n return $uibModal.open({\r\n templateUrl: 'static/html/search-state.html',\r\n controller: SearchUpdateModalInstanceCtrl,\r\n size: \"md\",\r\n backdrop: \"static\",\r\n backdropClass: \"waiting-cursor\",\r\n resolve: {\r\n searchRequestId: function () {\r\n return searchRequestId;\r\n },\r\n onCancel: function () {\r\n function cancel() {\r\n isSearchCancelled = true;\r\n }\r\n\r\n return cancel;\r\n }\r\n }\r\n });\r\n };\r\n\r\n $scope.goToSearchUrl = function () {\r\n //State params (query parameters) should all be lowercase\r\n var stateParams = {};\r\n stateParams.mode = $scope.category.searchType.toLowerCase();\r\n stateParams.imdbId = $scope.selectedItem === null ? null : $scope.selectedItem.imdbId;\r\n stateParams.tmdbId = $scope.selectedItem === null ? null : $scope.selectedItem.tmdbId;\r\n stateParams.tvdbId = $scope.selectedItem === null ? null : $scope.selectedItem.tvdbId;\r\n stateParams.tvrageId = $scope.selectedItem === null ? null : $scope.selectedItem.tvrageId;\r\n stateParams.tvmazeId = $scope.selectedItem === null ? null : $scope.selectedItem.tvmazeId;\r\n stateParams.title = $scope.selectedItem === null ? null : $scope.selectedItem.title;\r\n stateParams.season = $scope.season;\r\n stateParams.episode = $scope.episode;\r\n stateParams.query = $scope.query;\r\n stateParams.minsize = $scope.minsize;\r\n stateParams.maxsize = $scope.maxsize;\r\n stateParams.minage = $scope.minage;\r\n stateParams.maxage = $scope.maxage;\r\n stateParams.category = $scope.category.name;\r\n stateParams.indexers = encodeURIComponent($scope.selectedIndexers.join(\",\"));\r\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.repeatSearch = function (request) {\r\n var stateParams = SearchHistoryService.getStateParamsForRepeatedSearch(request);\r\n stateParams.indexers = encodeURIComponent($scope.selectedIndexers.join(\",\"));\r\n $state.go(\"root.search\", stateParams, {inherit: false, notify: true, reload: true});\r\n };\r\n\r\n $scope.searchBoxTooltip = \"Prefix terms with -- to exclude'\";\r\n $scope.$watchGroup(['isAskById', 'selectedItem'], function () {\r\n if (!$scope.isAskById) {\r\n $scope.searchBoxTooltip = \"Prefix terms with -- to exclude\";\r\n } else if ($scope.selectedItem === null) {\r\n $scope.searchBoxTooltip = \"Enter search terms for autocomplete\";\r\n } else {\r\n $scope.searchBoxTooltip = \"Enter additional search terms to limit the query\";\r\n }\r\n });\r\n\r\n $scope.clearAutocomplete = function () {\r\n $scope.selectedItem = null;\r\n $scope.query = \"\"; //Input is now for autocomplete and not for limiting the results\r\n focus('searchfield');\r\n };\r\n\r\n $scope.clearQuery = function () {\r\n $scope.selectedItem = null;\r\n $scope.query = \"\";\r\n focus('searchfield');\r\n };\r\n\r\n function calculateWidth(text) {\r\n var canvas = calculateWidth.canvas || (calculateWidth.canvas = document.createElement(\"canvas\"));\r\n var context = canvas.getContext(\"2d\");\r\n context.font = \"13px Roboto\";\r\n return context.measureText(text).width;\r\n }\r\n\r\n $scope.selectAutocompleteItem = function ($item) {\r\n $scope.selectedItem = $item;\r\n $scope.query = \"\";\r\n epochEnter = (new Date).getTime();\r\n var width = calculateWidth($item.title) + 30;\r\n $scope.selectedItemWidth = width + \"px\";\r\n };\r\n\r\n $scope.initiateSearch = function () {\r\n if ($scope.selectedIndexers.length === 0) {\r\n growl.error(\"You didn't select any indexers\");\r\n return;\r\n }\r\n if ($scope.selectedItem) {\r\n //Movie or tv show was selected\r\n $scope.goToSearchUrl();\r\n } else {\r\n //Simple query search\r\n $scope.goToSearchUrl();\r\n }\r\n };\r\n\r\n $scope.autocompleteActive = function () {\r\n return $scope.isAskById;\r\n };\r\n\r\n $scope.seriesSelected = function () {\r\n return $scope.category.searchType === \"TVSEARCH\";\r\n };\r\n\r\n $scope.toggleIndexer = function (indexer) {\r\n $scope.availableIndexers[indexer.name].activated = !$scope.availableIndexers[indexer.name].activated;\r\n };\r\n\r\n function isIndexerPreselected(indexer) {\r\n if (angular.isUndefined($scope.indexers)) {\r\n return indexer.preselect;\r\n } else {\r\n return _.contains($scope.indexers, indexer.name);\r\n }\r\n }\r\n\r\n function getAvailableIndexers() {\r\n var alreadySelected = $scope.selectedIndexers;\r\n var previouslyAvailable = _.pluck($scope.availableIndexers, \"name\");\r\n $scope.selectedIndexers = [];\r\n var availableIndexersList = _.chain(safeConfig.indexers).filter(function (indexer) {\r\n if (!indexer.showOnSearch) {\r\n return false;\r\n }\r\n var categorySelectedForIndexer = (angular.isUndefined(indexer.categories) || indexer.categories.length === 0 || $scope.category.name.toLowerCase() === \"all\" || indexer.categories.indexOf($scope.category.name) > -1);\r\n return categorySelectedForIndexer;\r\n }).sortBy(function (indexer) {\r\n return indexer.name.toLowerCase();\r\n })\r\n .map(function (indexer) {\r\n return {\r\n name: indexer.name,\r\n activated: isIndexerPreselected(indexer),\r\n preselect: indexer.preselect,\r\n categories: indexer.categories,\r\n searchModuleType: indexer.searchModuleType\r\n };\r\n }).value();\r\n _.forEach(availableIndexersList, function (x) {\r\n var deselectedBefore = (_.indexOf(previouslyAvailable, x.name) > -1 && _.indexOf(alreadySelected, x.name) === -1);\r\n var selectedBefore = (_.indexOf(previouslyAvailable, x.name) > -1 && _.indexOf(alreadySelected, x.name) > -1);\r\n if ((x.activated && !deselectedBefore) || selectedBefore) {\r\n $scope.selectedIndexers.push(x.name);\r\n }\r\n });\r\n return availableIndexersList;\r\n }\r\n\r\n\r\n $scope.formatRequest = function (request) {\r\n return $sce.trustAsHtml(SearchHistoryService.formatRequest(request, false, true, true, true));\r\n };\r\n\r\n $scope.availableIndexers = getAvailableIndexers();\r\n\r\n function getAndSetSearchRequests() {\r\n SearchHistoryService.getSearchHistoryForSearching().then(function (response) {\r\n $scope.searchHistory = response.searchRequests;\r\n });\r\n }\r\n\r\n if ($scope.mode) {\r\n $scope.startSearch();\r\n } else {\r\n //Getting the search history only makes sense when we're not currently searching\r\n _.defer(getAndSetSearchRequests);\r\n }\r\n\r\n $scope.$on(\"searchResultsShown\", function () {\r\n _.defer(getAndSetSearchRequests); //Defer because otherwise the results are only shown when this returns which may take a while with big databases\r\n });\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('SearchUpdateModalInstanceCtrl', SearchUpdateModalInstanceCtrl);\r\n\r\nfunction SearchUpdateModalInstanceCtrl($scope, $interval, SearchService, $uibModalInstance, searchRequestId, onCancel, bootstrapped) {\r\n\r\n var loggedSearchFinished = false;\r\n $scope.messages = [];\r\n $scope.indexerSelectionFinished = false;\r\n $scope.indexersSelected = 0;\r\n $scope.indexersFinished = 0;\r\n\r\n var socket = new SockJS(bootstrapped.baseUrl + 'websocket');\r\n var stompClient = Stomp.over(socket);\r\n stompClient.debug = null;\r\n stompClient.connect({}, function (frame) {\r\n stompClient.subscribe('/topic/searchState', function (message) {\r\n var data = JSON.parse(message.body);\r\n if (searchRequestId !== data.searchRequestId) {\r\n return;\r\n }\r\n $scope.searchFinished = data.searchFinished;\r\n $scope.indexersSelected = data.indexersSelected;\r\n $scope.indexersFinished = data.indexersFinished;\r\n $scope.progressMax = data.indexersSelected;\r\n if ($scope.progressMax > data.indexersSelected) {\r\n $scope.progressMax = \">=\" + data.indexersSelected;\r\n }\r\n if (data.messages) {\r\n $scope.messages = data.messages;\r\n }\r\n if ($scope.searchFinished && !loggedSearchFinished) {\r\n $scope.messages.push(\"Finished searching. Preparing results...\");\r\n loggedSearchFinished = true;\r\n }\r\n });\r\n });\r\n\r\n $scope.cancelSearch = function () {\r\n onCancel();\r\n $uibModalInstance.dismiss();\r\n };\r\n\r\n $scope.hasResults = function (message) {\r\n return /^[^0]\\d+.*/.test(message);\r\n };\r\n\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').directive('draggable', ['$rootScope', function ($rootScope) {\r\n return {\r\n restrict: 'A',\r\n link: function (scope, el, attrs, controller) {\r\n\r\n el.bind(\"dragstart\", function (e) {\r\n $rootScope.$emit(\"searchHistoryDrag\", el.attr(\"data-request\"));\r\n $rootScope.$broadcast(\"searchHistoryDrag\", el.attr(\"data-request\"));\r\n });\r\n }\r\n }\r\n}]);\r\n\r\n","\r\nRestartService.$inject = [\"growl\", \"NzbHydraControlService\", \"$uibModal\"];\r\nRestartModalInstanceCtrl.$inject = [\"$scope\", \"$timeout\", \"$http\", \"$window\", \"RequestsErrorHandler\", \"message\", \"baseUrl\"];angular\r\n .module('nzbhydraApp')\r\n .factory('RestartService', RestartService);\r\n\r\nfunction RestartService(growl, NzbHydraControlService, $uibModal) {\r\n\r\n return {\r\n restart: restart,\r\n startCountdown: startCountdown\r\n };\r\n\r\n function restart(message) {\r\n NzbHydraControlService.restart().then(function (response) {\r\n startCountdown(message, response.data.message);\r\n }, function () {\r\n growl.info(\"Unable to send restart command.\");\r\n })\r\n }\r\n\r\n function startCountdown(message, baseUrl) {\r\n $uibModal.open({\r\n templateUrl: 'static/html/restart-modal.html',\r\n controller: RestartModalInstanceCtrl,\r\n size: \"md\",\r\n backdrop: 'static',\r\n keyboard: false,\r\n resolve: {\r\n message: function () {\r\n return message;\r\n },\r\n baseUrl: function () {\r\n return baseUrl;\r\n }\r\n }\r\n });\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('RestartModalInstanceCtrl', RestartModalInstanceCtrl);\r\n\r\nfunction RestartModalInstanceCtrl($scope, $timeout, $http, $window, RequestsErrorHandler, message, baseUrl) {\r\n\r\n message = (angular.isDefined(message) ? message : \"\");\r\n $scope.message = message + \"Will reload page when NZBHydra is back\";\r\n $scope.baseUrl = baseUrl;\r\n $scope.pingUrl = angular.isDefined(baseUrl) ? (baseUrl + \"/internalapi/control/ping\") : \"internalapi/control/ping\";\r\n\r\n $scope.internalCaR = function (message, timer) {\r\n if (timer === 45) {\r\n $scope.message = message + \" Restarting takes longer than expected. You might want to check the log to see what's going on.\";\r\n } else {\r\n $scope.message = message + \" Will reload page when NZBHydra is back.\";\r\n $timeout(function () {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n $http.get($scope.pingUrl, {ignoreLoadingBar: true}).then(\r\n function () {\r\n $timeout(function () {\r\n $scope.message = \"Reloading page...\";\r\n if (angular.isDefined($scope.baseUrl)) {\r\n $window.location.href = $scope.baseUrl;\r\n } else {\r\n $window.location.reload();\r\n }\r\n }, 2000); //Give Hydra some time to load in the background, it might return the ping but not be completely up yet\r\n }, function () {\r\n $scope.internalCaR(message, timer + 1);\r\n });\r\n });\r\n }, 1000);\r\n $scope.message = message + \" Will reload page when NZBHydra is back.\";\r\n }\r\n };\r\n\r\n //Wait three seconds because otherwise the currently running instance will be found\r\n $timeout(function () {\r\n $scope.internalCaR(message, 0);\r\n }, 3000)\r\n}","\r\nNzbHydraControlService.$inject = [\"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('NzbHydraControlService', NzbHydraControlService);\r\n\r\nfunction NzbHydraControlService($http) {\r\n\r\n return {\r\n restart: restart,\r\n shutdown: shutdown\r\n };\r\n\r\n function restart() {\r\n return $http.get(\"internalapi/control/restart\");\r\n }\r\n\r\n function shutdown() {\r\n return $http.get(\"internalapi/control/shutdown\");\r\n }\r\n\r\n}\r\n","\r\nNzbDownloadService.$inject = [\"$http\", \"ConfigService\", \"DownloaderCategoriesService\"];angular\r\n .module('nzbhydraApp')\r\n .factory('NzbDownloadService', NzbDownloadService);\r\n\r\nfunction NzbDownloadService($http, ConfigService, DownloaderCategoriesService) {\r\n\r\n var service = {\r\n download: download,\r\n getEnabledDownloaders: getEnabledDownloaders\r\n };\r\n\r\n return service;\r\n\r\n function sendNzbAddCommand(downloader, searchResults, category) {\r\n var params = {\r\n downloaderName: downloader.name,\r\n searchResults: searchResults,\r\n category: category\r\n };\r\n return $http.put(\"internalapi/downloader/addNzbs\", params);\r\n }\r\n\r\n function download(downloader, searchResults, alwaysAsk) {\r\n var category = downloader.defaultCategory;\r\n if (alwaysAsk || (_.isNullOrEmpty(category) && category !== \"Use original category\") && category !== \"Use mapped category\" && category !== \"Use no category\") {\r\n return DownloaderCategoriesService.openCategorySelection(downloader).then(function (category) {\r\n return sendNzbAddCommand(downloader, searchResults, category);\r\n }, function (result) {\r\n return result;\r\n });\r\n } else {\r\n return sendNzbAddCommand(downloader, searchResults, category)\r\n }\r\n }\r\n\r\n function getEnabledDownloaders() {\r\n return _.filter(ConfigService.getSafe().downloading.downloaders, \"enabled\");\r\n }\r\n}\r\n\r\n","\nNotificationService.$inject = [\"$http\"];angular\n .module('nzbhydraApp')\n .service('NotificationService', NotificationService);\n\nfunction NotificationService($http) {\n\n var eventTypesData = {\n AUTH_FAILURE: {\n readable: \"Auth failure\",\n titleTemplate: \"Auth failure\",\n bodyTemplate: \"NZBHydra: A login for username $username$ failed. IP: $ip$.\",\n templateHelp: \"Available variables: $username$, $ip$.\",\n messageType: \"FAILURE\"\n },\n RESULT_DOWNLOAD: {\n readable: \"NZB download\",\n titleTemplate: \"NZB download\",\n bodyTemplate: \"NZBHydra: The result \\\"$title$\\\" was grabbed from indexer $indexerName$.\",\n templateHelp: \"Available variables: $title, $indexerName$, $source$ (NZB or torrent), $age$ ([] for torrents).\",\n messageType: \"INFO\"\n },\n RESULT_DOWNLOAD_COMPLETION: {\n readable: \"Download completion\",\n titleTemplate: \"Download completion\",\n bodyTemplate: \"NZBHydra: Download of \\\"$title$\\\" has finished. Download result: $downloadResult$.\",\n templateHelp: \"Requires the downloading tool to be configured. Available variables: $title, $downloadResult$.\",\n messageType: \"INFO\"\n },\n INDEXER_DISABLED: {\n readable: \"Indexer disabled\",\n titleTemplate: \"Indexer disabled\",\n bodyTemplate: \"NZBHydra: Indexer $indexerName$ was disabled (state: $state$). Message:\\n$message$.\",\n templateHelp: \"Available variables: $indexerName$, $state$, $message$.\",\n messageType: \"WARNING\"\n },\n INDEXER_REENABLED: {\n readable: \"Indexer reenabled after error\",\n titleTemplate: \"Indexer reenabled after error\",\n bodyTemplate: \"NZBHydra: Indexer $indexerName$ was reenabled after a previous error. It had been disabled since $disabledAt$.\",\n templateHelp: \"Available variables: $indexerName$, $disabledAt$.\",\n messageType: \"SUCCESS\"\n },\n UPDATE_INSTALLED: {\n readable: \"Automatic update installed\",\n titleTemplate: \"Update installed\",\n bodyTemplate: \"NZBHydra: A new version of was installed: $version$\",\n templateHelp: \"Available variables: $version$.\",\n messageType: \"SUCCESS\"\n },\n VIP_RENEWAL_REQUIRED: {\n readable: \"VIP renewal required (14 day warning)\",\n titleTemplate: \"VIP renewal required\",\n bodyTemplate: \"NZBHydra: VIP access for indexer $indexerName$ will run out soon: $expirationDate$.\",\n templateHelp: \"Available variables: $indexerName$, $expirationDate$.\",\n messageType: \"WARNING\"\n }\n }\n\n this.getAllEventTypes = function () {\n return _.keys(eventTypesData);\n };\n\n this.getAllData = function () {\n return eventTypesData;\n };\n\n this.humanize = function (eventType) {\n return eventTypesData[eventType].readable;\n };\n\n this.getTemplateHelp = function (eventType) {\n return eventTypesData[eventType].templateHelp;\n };\n\n this.getTitleTemplate = function (eventType) {\n return eventTypesData[eventType].titleTemplate;\n };\n\n this.getBodyTemplate = function (eventType) {\n return eventTypesData[eventType].bodyTemplate;\n };\n\n this.testNotification = function (eventType) {\n return $http.get('internalapi/notifications/test/' + eventType);\n }\n\n\n}","\nNotificationHistoryController.$inject = [\"$scope\", \"StatsService\", \"preloadData\", \"ConfigService\", \"$timeout\", \"NotificationService\"];angular\n .module('nzbhydraApp')\n .controller('NotificationHistoryController', NotificationHistoryController);\n\n\nfunction NotificationHistoryController($scope, StatsService, preloadData, ConfigService, $timeout, NotificationService) {\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n var sortModel = {\n column: \"time\",\n sortMode: 2\n };\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n }, 10);\n $scope.filterModel = {};\n\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\n\n\n //Preloaded data\n $scope.notifications = preloadData.notifications;\n $scope.totalNotifications = preloadData.totalNotifications;\n\n\n $scope.columnSizes = {\n time: 10,\n type: 15,\n title: 15,\n body: 40,\n urls: 20\n };\n\n $scope.update = function () {\n StatsService.getNotificationHistory($scope.pagination.current, $scope.limit, $scope.filterModel, sortModel).then(function (data) {\n $scope.notifications = data.notifications;\n $scope.totalNotifications = data.totalNotifications;\n });\n };\n\n\n $scope.eventTypesForFiltering = [];\n var eventTypes = NotificationService.getAllEventTypes();\n _.each(eventTypes, function (key) {\n $scope.eventTypesForFiltering.push({label: NotificationService.humanize(key), id: key})\n })\n\n $scope.$on(\"sort\", function (event, column, sortMode) {\n if (sortMode === 0) {\n column = \"time\";\n sortMode = 2;\n }\n sortModel = {\n column: column,\n sortMode: sortMode\n };\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n $scope.update();\n });\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filterValue) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n $scope.update();\n })\n\n $scope.formatEventType = function (notification) {\n return NotificationService.humanize(notification.notificationEventType);\n };\n\n $scope.formatEventBody = function (notification) {\n return notification.body.replace(\"\\n\", \"
              \");\n };\n\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateEpoch', reformatDateEpoch);\n\nfunction reformatDateEpoch() {\n return function (date) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm\");\n\n }\n}\n","\r\nModalService.$inject = [\"$uibModal\"];\r\nModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"headline\", \"message\", \"params\", \"textAlign\"];angular\r\n .module('nzbhydraApp')\r\n .factory('ModalService', ModalService);\r\n\r\nfunction ModalService($uibModal) {\r\n\r\n return {\r\n open: open\r\n };\r\n\r\n function open(headline, message, params, size, textAlign) {\r\n //params example:\r\n /*\r\n var p =\r\n {\r\n yes: {\r\n text: \"Yes\", //default: Ok\r\n onYes: function() {}\r\n },\r\n no: { //default: Empty\r\n text: \"No\",\r\n onNo: function () {\r\n }\r\n },\r\n cancel: {\r\n text: \"Cancel\", //default: Cancel\r\n onCancel: function () {\r\n }\r\n }\r\n };\r\n */\r\n if (angular.isUndefined(textAlign)) {\r\n textAlign = \"center\";\r\n }\r\n var modalInstance = $uibModal.open({\r\n templateUrl: 'static/html/modal.html',\r\n controller: 'ModalInstanceCtrl',\r\n size: angular.isDefined(size) ? size : \"md\",\r\n resolve: {\r\n headline: function () {\r\n return headline;\r\n },\r\n message: function () {\r\n return message;\r\n },\r\n params: function () {\r\n return params;\r\n },\r\n textAlign: function () {\r\n return textAlign;\r\n }\r\n }\r\n });\r\n\r\n modalInstance.result.then(function () {\r\n\r\n }, function () {\r\n\r\n });\r\n }\r\n\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp')\r\n .controller('ModalInstanceCtrl', ModalInstanceCtrl);\r\n\r\nfunction ModalInstanceCtrl($scope, $uibModalInstance, headline, message, params, textAlign) {\r\n\r\n $scope.message = message;\r\n $scope.headline = headline;\r\n $scope.params = params;\r\n $scope.showCancel = angular.isDefined(params) && angular.isDefined(params.cancel);\r\n $scope.showNo = angular.isDefined(params) && angular.isDefined(params.no);\r\n $scope.textAlign = textAlign;\r\n\r\n if (angular.isUndefined(params) || angular.isUndefined(params.yes)) {\r\n $scope.params = {\r\n yes: {\r\n text: \"Ok\"\r\n }\r\n }\r\n } else if (angular.isUndefined(params.yes.text)) {\r\n params.yes.text = \"Yes\";\r\n }\r\n\r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isUndefined($scope.params.no.text)) {\r\n $scope.params.no.text = \"No\";\r\n }\r\n\r\n if (angular.isDefined(params) && angular.isDefined(params.cancel) && angular.isUndefined($scope.params.cancel.text)) {\r\n $scope.params.cancel.text = \"Cancel\";\r\n }\r\n\r\n $scope.yes = function () {\r\n $uibModalInstance.close();\r\n if (angular.isDefined(params) && angular.isDefined(params.yes) && angular.isDefined($scope.params.yes.onYes)) {\r\n $scope.params.yes.onYes();\r\n }\r\n };\r\n\r\n $scope.no = function () {\r\n $uibModalInstance.close();\r\n if (angular.isDefined(params) && angular.isDefined(params.no) && angular.isDefined($scope.params.no.onNo)) {\r\n $scope.params.no.onNo($uibModalInstance);\r\n }\r\n };\r\n\r\n $scope.cancel = function () {\r\n $uibModalInstance.dismiss();\r\n if (angular.isDefined(params.cancel) && angular.isDefined($scope.params.cancel.onCancel)) {\r\n $scope.params.cancel.onCancel();\r\n }\r\n };\r\n\r\n $scope.$on(\"modal.closing\", function (targetScope, reason, c) {\r\n if (reason == \"backdrop click\") {\r\n $scope.cancel();\r\n }\r\n });\r\n}\r\n","angular\n .module('nzbhydraApp')\n .service('GeneralModalService', GeneralModalService);\n\nfunction GeneralModalService() {\n\n\n this.open = function (msg, template, templateUrl, size, data) {\n\n //Prevent circular dependency\n var myInjector = angular.injector([\"ng\", \"ui.bootstrap\"]);\n var $uibModal = myInjector.get(\"$uibModal\");\n var params = {};\n\n if (angular.isUndefined(size)) {\n params[\"size\"] = size;\n }\n if (angular.isUndefined(template)) {\n if (angular.isUndefined(templateUrl)) {\n params[\"template\"] = '
              ' + msg + '
              ';\n } else {\n params[\"templateUrl\"] = templateUrl;\n }\n } else {\n params[\"template\"] = template;\n }\n params[\"resolve\"] =\n {\n data: function () {\n return data;\n }\n };\n\n var modalInstance = $uibModal.open(params);\n\n modalInstance.result.then();\n\n };\n\n\n}","\nMigrationService.$inject = [\"$uibModal\"];\nMigrationModalInstanceCtrl.$inject = [\"$scope\", \"$uibModalInstance\", \"$interval\", \"$http\", \"blockUI\", \"ModalService\"];angular\n .module('nzbhydraApp')\n .factory('MigrationService', MigrationService);\n\nfunction MigrationService($uibModal) {\n\n return {\n migrate: migrate\n };\n\n function migrate() {\n var modalInstance = $uibModal.open({\n templateUrl: 'static/html/migration-modal.html',\n controller: 'MigrationModalInstanceCtrl',\n size: \"md\",\n backdrop: 'static',\n keyboard: false\n });\n\n modalInstance.result.then(function () {\n ConfigService.reloadConfig();\n }, function () {\n });\n }\n}\n\nangular\n .module('nzbhydraApp')\n .controller('MigrationModalInstanceCtrl', MigrationModalInstanceCtrl);\n\nfunction MigrationModalInstanceCtrl($scope, $uibModalInstance, $interval, $http, blockUI, ModalService) {\n\n $scope.baseUrl = \"http://127.0.0.1:5075\";\n\n $scope.foo = {isMigrating: false, baseUrl: $scope.baseUrl};\n $scope.doMigrateDatabase = true;\n\n $scope.yes = function () {\n var params;\n var url;\n if ($scope.foo.baseUrl && $scope.foo.isFileBasedOpen) {\n $scope.foo.baseUrl = null;\n }\n\n\n if ($scope.foo.isUrlBasedOpen) {\n url = \"internalapi/migration/url\";\n params = {baseurl: $scope.foo.baseUrl, doMigrateDatabase: $scope.doMigrateDatabase};\n if (!params.baseurl) {\n $scope.foo.isMigrating = false;\n ModalService.open(\"Requirements not met\", \"You did not enter a URL\", {\n yes: {\n text: \"OK\"\n }\n });\n return;\n }\n } else {\n url = \"internalapi/migration/files\";\n params = {\n settingsCfgFile: $scope.foo.settingsCfgFile,\n dbFile: $scope.foo.nzbhydraDbFile,\n doMigrateDatabase: $scope.doMigrateDatabase\n };\n if (!params.settingsCfgFile || (!params.dbFile && params.doMigrateDatabase)) {\n $scope.foo.isMigrating = false;\n ModalService.open(\"Requirements not met\", \"You did not enter all required valued\", {\n yes: {\n text: \"OK\"\n }\n });\n return;\n }\n }\n\n $scope.foo.isMigrating = true;\n\n var updateMigrationMessagesInterval = $interval(function () {\n $http.get(\"internalapi/migration/messages\").then(function (response) {\n $scope.foo.messages = response.data;\n },\n function () {\n $interval.cancel(updateMigrationMessagesInterval);\n $scope.foo.isMigrating = false;\n }\n );\n }, 500);\n\n $http.get(url, {params: params}).then(function (response) {\n var message;\n blockUI.stop();\n var data = response.data;\n if (!data.requirementsMet) {\n $interval.cancel(updateMigrationMessagesInterval);\n $scope.foo.isMigrating = false;\n ModalService.open(\"Requirements not met\", \"An error occurred while preparing the migration:
              \" + data.error, {\n yes: {\n text: \"OK\"\n }\n });\n } else if (!data.configMigrated) {\n $interval.cancel(updateMigrationMessagesInterval);\n $uibModalInstance.dismiss();\n $scope.foo.isMigrating = false;\n ModalService.open(\"Config migration failed\", \"An error occurred while migrating the config. Migration failed:
              \" + data.error, {\n yes: {\n text: \"OK\"\n }\n });\n } else if (!data.databaseMigrated) {\n $interval.cancel(updateMigrationMessagesInterval);\n $uibModalInstance.dismiss();\n $scope.foo.isMigrating = false;\n message = \"An error occurred while migrating the database.
              \" + data.error + \"
              . The config was migrated successfully though.\";\n if (data.messages.length > 0) {\n message += '

              The following warnings resulted from the config migration:
                ';\n _.forEach(data.messages, function (msg) {\n message += \"
              • \" + msg + \"
              • \";\n });\n message += \"
              \";\n }\n ModalService.open(\"Database migration failed\", message, {\n yes: {\n text: \"OK\"\n }\n });\n } else {\n $interval.cancel(updateMigrationMessagesInterval);\n $uibModalInstance.dismiss();\n $scope.foo.isMigrating = false;\n message = \"The migration was completed successfully.\";\n if (data.warningMessages.length > 0) {\n message += '

              The following warnings resulted from the config migration:
                ';\n _.forEach(data.warningMessages, function (msg) {\n message += \"
              • \" + msg + \"
              • \";\n });\n message += \"
              \";\n }\n message += \"

              NZBHydra needs to restart for the changes to be effective.\";\n ModalService.open(\"Migration successful\", message, {\n yes: {\n onYes: function () {\n RestartService.restart();\n },\n text: \"Restart\"\n },\n cancel: {\n onCancel: function () {\n\n },\n text: \"Not now\"\n }\n });\n }\n }, function (response) {\n $interval.cancel(updateMigrationMessagesInterval);\n $scope.foo.isMigrating = false;\n $scope.foo.messages = [response.data.message];\n }\n );\n\n $scope.$on('$destroy', function () {\n if (angular.isDefined(updateMigrationMessagesInterval)) {\n $interval.cancel(updateMigrationMessagesInterval);\n }\n });\n\n };\n\n $scope.cancel = function () {\n $uibModalInstance.dismiss();\n };\n\n}\n","\r\nLoginController.$inject = [\"$scope\", \"RequestsErrorHandler\", \"$state\", \"HydraAuthService\", \"growl\"];angular\r\n .module('nzbhydraApp')\r\n .controller('LoginController', LoginController);\r\n\r\nfunction LoginController($scope, RequestsErrorHandler, $state, HydraAuthService, growl) {\r\n $scope.user = {};\r\n $scope.login = function () {\r\n RequestsErrorHandler.specificallyHandled(function () {\r\n HydraAuthService.login($scope.user.username, $scope.user.password).then(function () {\r\n HydraAuthService.setLoggedInByForm();\r\n growl.info(\"Login successful!\");\r\n $state.go(\"root.search\");\r\n }, function () {\r\n growl.error(\"Login failed!\")\r\n });\r\n });\r\n }\r\n}\r\n","\nIndexerStatusesController.$inject = [\"$scope\", \"$http\", \"statuses\"];\nformatDate.$inject = [\"dateFilter\"];angular\n .module('nzbhydraApp')\n .controller('IndexerStatusesController', IndexerStatusesController);\n\nfunction IndexerStatusesController($scope, $http, statuses) {\n $scope.statuses = statuses.data;\n $scope.expiryWarnings = {};\n\n $scope.formatState = function (state) {\n if (state === \"ENABLED\") {\n return \"Enabled\";\n } else if (state === \"DISABLED_SYSTEM_TEMPORARY\") {\n return \"Temporarily disabled by system\";\n } else if (state === \"DISABLED_SYSTEM\") {\n return \"Disabled by system\";\n } else {\n return \"Disabled by user\";\n }\n };\n\n $scope.getLabelClass = function (state) {\n if (state === \"ENABLED\") {\n return \"primary\";\n } else if (state === \"DISABLED_SYSTEM_TEMPORARY\") {\n return \"warning\";\n } else if (state === \"DISABLED_SYSTEM\") {\n return \"danger\";\n } else {\n return \"default\";\n }\n };\n\n $scope.isInPast = function (epochSeconds) {\n return epochSeconds < moment().unix();\n };\n\n\n _.each($scope.statuses, function (status) {\n if (status.vipExpirationDate != null && status.vipExpirationDate !== \"Lifetime\") {\n var expiryDate = moment(status.vipExpirationDate, \"YYYY-MM-DD\");\n var messagePrefix = \"VIP access\";\n if (expiryDate < moment()) {\n status.expiryWarning = messagePrefix + \" expired\";\n } else if (expiryDate.subtract(7, 'days') < moment()) {\n status.expiryWarning = messagePrefix + \" will expire in the next 7 days\";\n }\n console.log(status.expiryWarning);\n }\n }\n )\n ;\n}\n\nangular\n .module('nzbhydraApp')\n .filter('formatDate', formatDate);\n\nfunction formatDate(dateFilter) {\n return function (timestamp, hidePast) {\n if (timestamp) {\n if (timestamp * 1000 < (new Date).getTime() && hidePast) {\n return \"\"; //\n }\n\n var t = timestamp * 1000;\n t = dateFilter(t, 'yyyy-MM-dd HH:mm');\n return t;\n } else {\n return \"\";\n }\n }\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDate', reformatDate);\n\nfunction reformatDate() {\n return function (date, format) {\n if (!date) {\n return \"\";\n }\n if (angular.isUndefined(format)) {\n format = \"YYYY-MM-DD HH:mm\";\n }\n //Date in database is saved as UTC without timezone information\n return moment.unix(date).local().format(format);\n }\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateSeconds', reformatDateSeconds);\n\nfunction reformatDateSeconds() {\n return function (date, format) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm:ss\");\n }\n}\n\n\nangular\n .module('nzbhydraApp')\n .filter('humanizeDate', humanizeDate);\n\nfunction humanizeDate() {\n return function (date) {\n return moment().to(moment.unix(date));\n }\n}","\r\nIndexController.$inject = [\"$scope\", \"$http\", \"$stateParams\", \"$state\"];angular\r\n .module('nzbhydraApp')\r\n .controller('IndexController', IndexController);\r\n\r\nfunction IndexController($scope, $http, $stateParams, $state) {\r\n\r\n $state.go(\"root.search\");\r\n}\r\n","\r\nHydraAuthService.$inject = [\"$q\", \"$rootScope\", \"$http\", \"bootstrapped\", \"$httpParamSerializerJQLike\", \"$state\"];angular\r\n .module('nzbhydraApp')\r\n .factory('HydraAuthService', HydraAuthService);\r\n\r\nfunction HydraAuthService($q, $rootScope, $http, bootstrapped, $httpParamSerializerJQLike, $state) {\r\n\r\n var loggedIn = bootstrapped.username;\r\n\r\n\r\n return {\r\n isLoggedIn: isLoggedIn,\r\n login: login,\r\n askForPassword: askForPassword,\r\n logout: logout,\r\n setLoggedInByForm: setLoggedInByForm,\r\n getUserRights: getUserRights,\r\n setLoggedInByBasic: setLoggedInByBasic,\r\n getUserName: getUserName,\r\n getUserInfos: getUserInfos\r\n };\r\n\r\n function getUserInfos() {\r\n return bootstrapped;\r\n }\r\n\r\n function isLoggedIn() {\r\n return bootstrapped.username;\r\n }\r\n\r\n function setLoggedInByForm() {\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n }\r\n\r\n\r\n function setLoggedInByBasic(_maySeeStats, _maySeeAdmin, _username) {\r\n }\r\n\r\n function login(username, password) {\r\n var deferred = $q.defer();\r\n //return $http.post(\"login\", data = {username: username, password: password})\r\n return $http({\r\n url: \"login\",\r\n method: \"POST\",\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded' // Note the appropriate header\r\n },\r\n data: $httpParamSerializerJQLike({username: username, password: password})\r\n })\r\n .then(function () {\r\n $http.get(\"internalapi/userinfos\").then(function (data) {\r\n bootstrapped = data.data;\r\n loggedIn = true;\r\n $rootScope.$broadcast(\"user:loggedIn\");\r\n deferred.resolve();\r\n });\r\n });\r\n }\r\n\r\n function askForPassword(params) {\r\n return $http.get(\"internalapi/askpassword\", {params: params}).then(function (data) {\r\n bootstrapped = data.data;\r\n return bootstrapped;\r\n });\r\n }\r\n\r\n function logout() {\r\n var deferred = $q.defer();\r\n return $http.post(\"logout\").then(function () {\r\n $http.get(\"internalapi/userinfos\").then(function (data) {\r\n bootstrapped = data.data;\r\n $rootScope.$broadcast(\"user:loggedOut\");\r\n loggedIn = false;\r\n if (bootstrapped.maySeeSearch) {\r\n $state.go(\"root.search\");\r\n } else {\r\n $state.go(\"root.login\");\r\n }\r\n //window.location.reload(false);\r\n deferred.resolve();\r\n });\r\n });\r\n }\r\n\r\n function getUserRights() {\r\n var userInfos = getUserInfos();\r\n return {\r\n maySeeStats: userInfos.maySeeStats,\r\n maySeeAdmin: userInfos.maySeeAdmin,\r\n maySeeSearch: userInfos.maySeeSearch\r\n };\r\n }\r\n\r\n function getUserName() {\r\n return bootstrapped.username;\r\n }\r\n\r\n\r\n}","\nHeaderController.$inject = [\"$scope\", \"$state\", \"growl\", \"HydraAuthService\", \"bootstrapped\"];angular\n .module('nzbhydraApp')\n .controller('HeaderController', HeaderController);\n\nfunction HeaderController($scope, $state, growl, HydraAuthService, bootstrapped) {\n\n\n $scope.showLoginout = false;\n $scope.oldUserName = null;\n $scope.bootstrapped = bootstrapped;\n\n function update(event) {\n\n $scope.userInfos = HydraAuthService.getUserInfos();\n if (!$scope.userInfos.authConfigured) {\n $scope.showSearch = true;\n $scope.showAdmin = true;\n $scope.showStats = true;\n $scope.showLoginout = false;\n } else {\n if ($scope.userInfos.username) {\n $scope.showSearch = true;\n $scope.showAdmin = $scope.userInfos.maySeeAdmin || !$scope.userInfos.adminRestricted;\n $scope.showStats = $scope.userInfos.maySeeStats || !$scope.userInfos.statsRestricted;\n $scope.showLoginout = true;\n $scope.username = $scope.userInfos.username;\n $scope.loginlogoutText = \"Logout \" + $scope.username;\n $scope.oldUserName = $scope.username;\n } else {\n $scope.showAdmin = !$scope.userInfos.adminRestricted;\n $scope.showStats = !$scope.userInfos.statsRestricted;\n $scope.showSearch = !$scope.userInfos.searchRestricted;\n $scope.loginlogoutText = \"Login\";\n $scope.showLoginout = ($scope.userInfos.adminRestricted || $scope.userInfos.statsRestricted || $scope.userInfos.searchRestricted) && event !== \"loggedOut\" && !$state.is(\"root.login\");\n $scope.username = \"\";\n }\n }\n }\n\n update();\n\n\n $scope.$on(\"user:loggedIn\", function (event, data) {\n update(\"loggedIn\");\n });\n\n $scope.$on(\"user:loggedOut\", function (event, data) {\n update(\"loggedOut\");\n });\n\n $scope.loginout = function () {\n if (HydraAuthService.isLoggedIn()) {\n HydraAuthService.logout().then(function () {\n if ($scope.userInfos.authType === \"BASIC\") {\n growl.info(\"Logged out. Close your browser to make sure session is closed.\");\n }\n else if ($scope.userInfos.authType === \"FORM\") {\n growl.info(\"Logged out\");\n }\n update();\n //$state.go(\"root.search\", null, {reload: true});\n });\n\n } else {\n if ($scope.userInfos.authType === \"BASIC\") {\n var params = {};\n if ($scope.oldUserName) {\n params = {\n old_username: $scope.oldUserName\n }\n }\n HydraAuthService.askForPassword(params).then(function () {\n growl.info(\"Login successful!\");\n $scope.oldUserName = null;\n update(\"loggedIn\");\n $state.go(\"root.search\");\n })\n } else if ($scope.userInfos.authType === \"FORM\") {\n $state.go(\"root.login\");\n } else {\n growl.info(\"You shouldn't need to login but here you go!\");\n }\n }\n\n };\n}\n","/*\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n//\nGenericStorageService.$inject = [\"$http\"];\nangular\n .module('nzbhydraApp')\n .factory('GenericStorageService', GenericStorageService);\n\nfunction GenericStorageService($http) {\n\n return {\n get: get,\n put: put\n };\n\n function get(key, forUser) {\n return $http.get(\"internalapi/genericstorage/\" + key, {params: {forUser: forUser}, ignoreLoadingBar: true});\n }\n\n function put(key, forUser, value) {\n return $http.put(\"internalapi/genericstorage/\" + key, value, {params: {forUser: forUser}, ignoreLoadingBar: true});\n }\n\n\n}","var HEADER_NAME = 'NzbHydra2-Handle-Errors-Generically';\nvar specificallyHandleInProgress = false;\n\nnzbhydraapp.factory('RequestsErrorHandler', [\"$q\", \"growl\", \"blockUI\", \"GeneralModalService\", function ($q, growl, blockUI, GeneralModalService) {\n return {\n // --- The user's API for claiming responsiblity for requests ---\n specificallyHandled: function (specificallyHandledBlock) {\n specificallyHandleInProgress = true;\n try {\n return specificallyHandledBlock();\n } finally {\n specificallyHandleInProgress = false;\n }\n },\n\n // --- Response interceptor for handling errors generically ---\n responseError: function (rejection) {\n blockUI.reset();\n if (rejection.data instanceof ArrayBuffer) {\n //The case when the response was specifically requested as that, e.g. for debug infos\n rejection.data = JSON.parse(new TextDecoder().decode(rejection.data));\n }\n var shouldHandle = (rejection && rejection.config && rejection.status !== 403 && rejection.config.headers && rejection.config.headers[HEADER_NAME] && !rejection.config.url.contains(\"logerror\") && !rejection.config.url.contains(\"/ping\") && !rejection.config.alreadyHandled);\n if (shouldHandle) {\n if (rejection.data) {\n\n var message = \"An error occurred:
              \" + rejection.data.status;\n if (rejection.data.error) {\n message += \": \" + rejection.data.error\n }\n if (rejection.data.path) {\n message += \"

              Path: \" + rejection.data.path;\n }\n if (message !== \"No message available\") {\n message += \"

              Message: \" + rejection.data.message;\n } else {\n message += \"

              Exception: \" + rejection.data.exception;\n }\n } else {\n message = \"An unknown error occurred while communicating with NZBHydra:

              \" + JSON.stringify(rejection);\n }\n GeneralModalService.open(message);\n\n } else if (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME] && rejection.config.url.contains(\"logerror\")) {\n console.log(\"Not handling connection error while sending exception to server\");\n }\n\n return $q.reject(rejection);\n }\n };\n}]);\n\nnzbhydraapp.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {\n $httpProvider.interceptors.push('RequestsErrorHandler');\n\n // --- Decorate $http to add a special header by default ---\n\n function addHeaderToConfig(config) {\n config = config || {};\n config.headers = config.headers || {};\n\n // Add the header unless user asked to handle errors himself\n if (!specificallyHandleInProgress) {\n config.headers[HEADER_NAME] = true;\n }\n\n return config;\n }\n\n // The rest here is mostly boilerplate needed to decorate $http safely\n $provide.decorator('$http', ['$delegate', function ($delegate) {\n function decorateRegularCall(method) {\n return function (url, config) {\n return $delegate[method](url, addHeaderToConfig(config));\n };\n }\n\n function decorateDataCall(method) {\n return function (url, data, config) {\n return $delegate[method](url, data, addHeaderToConfig(config));\n };\n }\n\n function copyNotOverriddenAttributes(newHttp) {\n for (var attr in $delegate) {\n if (!newHttp.hasOwnProperty(attr)) {\n if (typeof($delegate[attr]) === 'function') {\n newHttp[attr] = function () {\n return $delegate.apply($delegate, arguments);\n };\n } else {\n newHttp[attr] = $delegate[attr];\n }\n }\n }\n }\n\n var newHttp = function (config) {\n return $delegate(addHeaderToConfig(config));\n };\n\n newHttp.get = decorateRegularCall('get');\n newHttp.delete = decorateRegularCall('delete');\n newHttp.head = decorateRegularCall('head');\n newHttp.jsonp = decorateRegularCall('jsonp');\n newHttp.post = decorateDataCall('post');\n newHttp.put = decorateDataCall('put');\n\n copyNotOverriddenAttributes(newHttp);\n\n return newHttp;\n }]);\n}]);\n","var filters = angular.module('filters', []);\n\nfilters.filter('bytes', function () {\n return function (bytes) {\n return filesize(bytes);\n }\n});\n\nfilters\n .filter('unsafe', ['$sce', function ($sce) {\n return function (text) {\n return $sce.trustAsHtml(text);\n };\n }]);\n\n","\r\nFileSelectionService.$inject = [\"$http\", \"$q\", \"$uibModal\"];angular\r\n .module('nzbhydraApp')\r\n .factory('FileSelectionService', FileSelectionService);\r\n\r\nfunction FileSelectionService($http, $q, $uibModal) {\r\n\r\n var categories = {};\r\n var selectedCategory = {};\r\n\r\n var service = {\r\n open: open\r\n };\r\n\r\n var deferred;\r\n\r\n return service;\r\n\r\n\r\n function open(fullPath, type) {\r\n var instance = $uibModal.open({\r\n templateUrl: 'static/html/file-selection.html',\r\n controller: 'FileSelectionModalController',\r\n size: \"md\",\r\n resolve: {\r\n data: function () {\r\n return $http.post(\"internalapi/config/folderlisting\", {\r\n fullPath: angular.isDefined(fullPath) ? fullPath : null,\r\n goUp: false,\r\n type: type\r\n });\r\n },\r\n type: function () {\r\n return type;\r\n }\r\n }\r\n });\r\n\r\n instance.result.then(function (selection) {\r\n deferred.resolve(selection);\r\n }, function () {\r\n deferred.reject(\"dismissed\");\r\n }\r\n );\r\n deferred = $q.defer();\r\n return deferred.promise;\r\n }\r\n\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').controller('FileSelectionModalController', [\"$scope\", \"$http\", \"$uibModalInstance\", \"FileSelectionService\", \"data\", \"type\", function ($scope, $http, $uibModalInstance, FileSelectionService, data, type) {\r\n\r\n $scope.type = type;\r\n $scope.showType = type === \"file\" ? \"File\" : \"Folder\";\r\n $scope.data = data.data;\r\n\r\n $scope.select = function (fileOrFolder, selectType) {\r\n if (selectType === \"file\" && type === \"file\") {\r\n $uibModalInstance.close(fileOrFolder.fullPath);\r\n } else if (selectType === \"folder\") {\r\n $http.post(\"internalapi/config/folderlisting\", {\r\n fullPath: fileOrFolder.fullPath,\r\n type: type,\r\n goUp: false\r\n }).then(function (data) {\r\n $scope.data = data.data;\r\n })\r\n }\r\n };\r\n\r\n $scope.goUp = function () {\r\n $http.post(\"internalapi/config/folderlisting\", {\r\n fullPath: $scope.data.fullPath,\r\n type: type,\r\n goUp: true\r\n }).then(function (data) {\r\n $scope.data = data.data;\r\n })\r\n };\r\n\r\n $scope.submit = function () {\r\n $uibModalInstance.close($scope.data.fullPath);\r\n }\r\n\r\n}]);","\r\nFileDownloadService.$inject = [\"$http\", \"growl\"];angular\r\n .module('nzbhydraApp')\r\n .factory('FileDownloadService', FileDownloadService);\r\n\r\nfunction FileDownloadService($http, growl) {\r\n\r\n var service = {\r\n downloadFile: downloadFile\r\n };\r\n\r\n return service;\r\n\r\n function downloadFile(link, filename, method, data) {\r\n return $http({\r\n method: method,\r\n url: link,\r\n data: data,\r\n responseType: 'arraybuffer'\r\n }).then(function (response, status, headers, config) {\r\n var a = document.createElement('a');\r\n var blob = new Blob([response.data], {'type': \"application/octet-stream\"});\r\n a.href = URL.createObjectURL(blob);\r\n a.download = filename;\r\n\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n }, function (data, status, headers, config) {\r\n growl.error(status);\r\n });\r\n\r\n }\r\n\r\n\r\n}\r\n\r\n","\r\nDownloaderCategoriesService.$inject = [\"$http\", \"$q\", \"$uibModal\"];angular\r\n .module('nzbhydraApp')\r\n .factory('DownloaderCategoriesService', DownloaderCategoriesService);\r\n\r\nfunction DownloaderCategoriesService($http, $q, $uibModal) {\r\n\r\n var categories = {};\r\n var selectedCategory = {};\r\n\r\n var service = {\r\n get: getCategories,\r\n invalidate: invalidate,\r\n select: select,\r\n openCategorySelection: openCategorySelection\r\n };\r\n\r\n var deferred;\r\n\r\n return service;\r\n\r\n function getCategories(downloader) {\r\n function loadAll() {\r\n if (downloader.name in categories) {\r\n var deferred = $q.defer();\r\n deferred.resolve(categories[downloader.name]);\r\n return deferred.promise;\r\n }\r\n\r\n return $http.get(encodeURI('internalapi/downloader/' + downloader.name + \"/categories\"))\r\n .then(function (categoriesResponse) {\r\n categories[downloader.name] = categoriesResponse.data;\r\n return categoriesResponse.data;\r\n\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n return loadAll().then(function (categories) {\r\n return categories;\r\n }, function (error) {\r\n throw error;\r\n });\r\n }\r\n\r\n\r\n function openCategorySelection(downloader) {\r\n var instance = $uibModal.open({\r\n templateUrl: 'static/html/directives/addable-nzb-modal.html',\r\n controller: 'DownloaderCategorySelectionController',\r\n size: \"sm\",\r\n resolve: {\r\n categories: function () {\r\n return getCategories(downloader)\r\n }\r\n }\r\n });\r\n\r\n instance.result.then(function () {\r\n }, function () {\r\n deferred.reject(\"dismissed\");\r\n }\r\n );\r\n deferred = $q.defer();\r\n return deferred.promise;\r\n }\r\n\r\n function select(category) {\r\n selectedCategory = category;\r\n\r\n deferred.resolve(category);\r\n }\r\n\r\n function invalidate() {\r\n categories = {};\r\n }\r\n}\r\n\r\nangular\r\n .module('nzbhydraApp').controller('DownloaderCategorySelectionController', [\"$scope\", \"$uibModalInstance\", \"DownloaderCategoriesService\", \"categories\", function ($scope, $uibModalInstance, DownloaderCategoriesService, categories) {\r\n\r\n $scope.categories = categories;\r\n categories.sort();\r\n console.log(categories);\r\n $scope.select = function (category) {\r\n DownloaderCategoriesService.select(category);\r\n $uibModalInstance.close($scope);\r\n }\r\n}]);","\nDownloadHistoryController.$inject = [\"$scope\", \"StatsService\", \"downloads\", \"ConfigService\", \"$timeout\", \"$sce\"];angular\n .module('nzbhydraApp')\n .controller('DownloadHistoryController', DownloadHistoryController);\n\n\nfunction DownloadHistoryController($scope, StatsService, downloads, ConfigService, $timeout, $sce) {\n $scope.limit = 100;\n $scope.pagination = {\n current: 1\n };\n var sortModel = {\n column: \"time\",\n sortMode: 2\n };\n $timeout(function () {\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n }, 10);\n $scope.filterModel = {};\n\n //Filter options\n $scope.indexersForFiltering = [];\n _.forEach(ConfigService.getSafe().indexers, function (indexer) {\n $scope.indexersForFiltering.push({label: indexer.name, id: indexer.name})\n });\n $scope.preselectedTimeInterval = {beforeDate: null, afterDate: null};\n $scope.statusesForFiltering = [\n {label: \"None\", id: 'NONE'},\n {label: \"Requested\", id: 'REQUESTED'},\n {label: \"Internal error\", id: 'INTERNAL_ERROR'},\n {label: \"NZB downloaded successful\", id: 'NZB_DOWNLOAD_SUCCESSFUL'},\n {label: \"NZB download error\", id: 'NZB_DOWNLOAD_ERROR'},\n {label: \"NZB added\", id: 'NZB_ADDED'},\n {label: \"NZB not added\", id: 'NZB_NOT_ADDED'},\n {label: \"NZB add error\", id: 'NZB_ADD_ERROR'},\n {label: \"NZB add rejected\", id: 'NZB_ADD_REJECTED'},\n {label: \"Content download successful\", id: 'CONTENT_DOWNLOAD_SUCCESSFUL'},\n {label: \"Content download warning\", id: 'CONTENT_DOWNLOAD_WARNING'},\n {label: \"Content download error\", id: 'CONTENT_DOWNLOAD_ERROR'}\n ];\n $scope.accessOptionsForFiltering = [{label: \"All\", value: \"all\"}, {label: \"API\", value: 'API'}, {\n label: \"Internal\",\n value: 'INTERNAL'\n }];\n\n //Preloaded data\n $scope.nzbDownloads = downloads.nzbDownloads;\n $scope.totalDownloads = downloads.totalDownloads;\n\n $scope.columnSizes = {\n time: 10,\n indexer: 10,\n title: 37,\n result: 9,\n source: 8,\n age: 6,\n username: 10,\n ip: 10\n };\n var anyUsername = false;\n var anyIp = false;\n for (var download in $scope.nzbDownloads) {\n if (download.username) {\n anyUsername = true;\n }\n if (download.ip) {\n anyIp = true;\n }\n if (anyIp && anyUsername) {\n break;\n }\n }\n\n if (ConfigService.getSafe().logging.historyUserInfoType === \"NONE\" || (!anyUsername && !anyIp)) {\n $scope.columnSizes.username = 0;\n $scope.columnSizes.ip = 0;\n $scope.columnSizes.title += 20;\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"IP\") {\n $scope.columnSizes.username = 0;\n $scope.columnSizes.title += 10;\n } else if (ConfigService.getSafe().logging.historyUserInfoType === \"USERNAME\") {\n $scope.columnSizes.ip = 0;\n $scope.columnSizes.title += 10;\n }\n\n\n $scope.update = function () {\n StatsService.getDownloadHistory($scope.pagination.current, $scope.limit, $scope.filterModel, sortModel).then(function (downloads) {\n $scope.nzbDownloads = downloads.nzbDownloads;\n $scope.totalDownloads = downloads.totalDownloads;\n });\n };\n\n\n $scope.$on(\"sort\", function (event, column, sortMode) {\n if (sortMode === 0) {\n column = \"time\";\n sortMode = 2;\n }\n sortModel = {\n column: column,\n sortMode: sortMode\n };\n $scope.$broadcast(\"newSortColumn\", sortModel.column, sortModel.sortMode);\n $scope.update();\n });\n\n $scope.getStatusIcon = function (result) {\n var spans;\n if (result === \"NONE\" || result === \"REQUESTED\") {\n spans = ''\n }\n if (result === \"INTERNAL_ERROR\") {\n spans = ''\n }\n if (result === \"INTERNAL_ERROR\") {\n spans = ''\n }\n if (result === 'NZB_DOWNLOAD_SUCCESSFUL') {\n spans = '';\n }\n if (result === 'NZB_DOWNLOAD_ERROR') {\n spans = '';\n }\n if (result === 'NZB_ADDED') {\n spans = '';\n }\n if (result === 'NZB_NOT_ADDED' || result === 'NZB_ADD_ERROR' || result === 'NZB_ADD_REJECTED') {\n spans = '';\n }\n if (result === 'CONTENT_DOWNLOAD_SUCCESSFUL') {\n spans = '';\n }\n if (result === 'CONTENT_DOWNLOAD_ERROR' || result === 'CONTENT_DOWNLOAD_WARNING') {\n spans = '';\n }\n return $sce.trustAsHtml('' + spans + '');\n\n };\n\n\n $scope.$on(\"filter\", function (event, column, filterModel, isActive) {\n if (filterModel.filterValue) {\n $scope.filterModel[column] = filterModel;\n } else {\n delete $scope.filterModel[column];\n }\n $scope.update();\n })\n\n}\n\nangular\n .module('nzbhydraApp')\n .filter('reformatDateEpoch', reformatDateEpoch);\n\nfunction reformatDateEpoch() {\n return function (date) {\n return moment.unix(date).local().format(\"YYYY-MM-DD HH:mm\");\n\n }\n}\n","/*\r\n * (C) Copyright 2017 TheOtherP (theotherp@posteo.net)\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n\r\nDebugService.$inject = [\"$filter\"];\r\nangular\r\n .module('nzbhydraApp')\r\n .factory('DebugService', DebugService);\r\n\r\nfunction DebugService($filter) {\r\n\r\n var debug = {};\r\n\r\n return {\r\n log: log,\r\n print: print\r\n };\r\n\r\n function log(name) {\r\n if (!(name in debug)) {\r\n debug[name] = {first: new Date().getTime(), last: new Date().getTime()};\r\n } else {\r\n debug[name][\"last\"] = new Date().getTime();\r\n }\r\n }\r\n\r\n function print() {\r\n //Re-enable if necessary\r\n // for (var key in debug) {\r\n // if (debug.hasOwnProperty(key)) {\r\n // console.log(\"First \" + key + \": \" + $filter(\"date\")(new Date(debug[key][\"first\"]), \"h:mm:ss:sss\"));\r\n // console.log(\"Last \" + key + \": \" + $filter(\"date\")(new Date(debug[key][\"last\"]), \"h:mm:ss:sss\"));\r\n // console.log(\"Diff: \" + (debug[key][\"last\"] - debug[key][\"first\"]));\r\n // }\r\n // }\r\n }\r\n\r\n\r\n}","\r\nCategoriesService.$inject = [\"ConfigService\"];angular\r\n .module('nzbhydraApp')\r\n .factory('CategoriesService', CategoriesService);\r\n\r\nfunction CategoriesService(ConfigService) {\r\n\r\n return {\r\n getByName: getByName,\r\n getAllCategories: getAllCategories,\r\n getDefault: getDefault,\r\n getWithoutAll: getWithoutAll\r\n };\r\n\r\n\r\n function getByName(name) {\r\n for (var cat in ConfigService.getSafe().categoriesConfig.categories) {\r\n var category = ConfigService.getSafe().categoriesConfig.categories[cat];\r\n if (category.name === name) {\r\n return category;\r\n }\r\n }\r\n }\r\n\r\n function getAllCategories() {\r\n return ConfigService.getSafe().categoriesConfig.categories;\r\n }\r\n\r\n function getWithoutAll() {\r\n var cats = ConfigService.getSafe().categoriesConfig.categories;\r\n return cats.slice(1, cats.length);\r\n }\r\n\r\n function getDefault() {\r\n return getByName(ConfigService.getSafe().categoriesConfig.defaultCategory);\r\n }\r\n\r\n}","\r\nBackupService.$inject = [\"$http\"];angular\r\n .module('nzbhydraApp')\r\n .factory('BackupService', BackupService);\r\n\r\nfunction BackupService($http) {\r\n\r\n return {\r\n getBackupsList: getBackupsList,\r\n restoreFromFile: restoreFromFile\r\n };\r\n\r\n\r\n function getBackupsList() {\r\n return $http.get('internalapi/backup/list').then(function (response) {\r\n return response.data;\r\n });\r\n }\r\n\r\n function restoreFromFile(filename) {\r\n return $http.get('internalapi/backup/restore', {params: {filename: filename}}).then(function (response) {\r\n return response;\r\n });\r\n }\r\n\r\n}","//Copied from https://github.com/oblador/angular-scroll because installing it via bower caused errors\nvar duScrollDefaultEasing = function (x) {\n\n\n if (x < 0.5) {\n return Math.pow(x * 2, 2) / 2;\n }\n return 1 - Math.pow((1 - x) * 2, 2) / 2;\n};\n\nvar duScroll = angular.module('duScroll', [\n 'duScroll.scrollspy',\n 'duScroll.smoothScroll',\n 'duScroll.scrollContainer',\n 'duScroll.spyContext',\n 'duScroll.scrollHelpers'\n])\n//Default animation duration for smoothScroll directive\n .value('duScrollDuration', 350)\n //Scrollspy debounce interval, set to 0 to disable\n .value('duScrollSpyWait', 100)\n //Scrollspy forced refresh interval, use if your content changes or reflows without scrolling.\n //0 to disable\n .value('duScrollSpyRefreshInterval', 0)\n //Wether or not multiple scrollspies can be active at once\n .value('duScrollGreedy', false)\n //Default offset for smoothScroll directive\n .value('duScrollOffset', 0)\n //Default easing function for scroll animation\n .value('duScrollEasing', duScrollDefaultEasing)\n //Which events on the container (such as body) should cancel scroll animations\n .value('duScrollCancelOnEvents', 'scroll mousedown mousewheel touchmove keydown')\n //Whether or not to activate the last scrollspy, when page/container bottom is reached\n .value('duScrollBottomSpy', false)\n //Active class name\n .value('duScrollActiveClass', 'active');\n\nif (typeof module !== 'undefined' && module && module.exports) {\n module.exports = duScroll;\n}\n\n\nangular.module('duScroll.scrollHelpers', ['duScroll.requestAnimation'])\n .run([\"$window\", \"$q\", \"cancelAnimation\", \"requestAnimation\", \"duScrollEasing\", \"duScrollDuration\", \"duScrollOffset\", \"duScrollCancelOnEvents\", function ($window, $q, cancelAnimation, requestAnimation, duScrollEasing, duScrollDuration, duScrollOffset, duScrollCancelOnEvents) {\n 'use strict';\n\n var proto = {};\n\n var isDocument = function (el) {\n return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE);\n };\n\n var isElement = function (el) {\n return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE);\n };\n\n var unwrap = function (el) {\n return isElement(el) || isDocument(el) ? el : el[0];\n };\n\n proto.duScrollTo = function (left, top, duration, easing) {\n var aliasFn;\n if (angular.isElement(left)) {\n aliasFn = this.duScrollToElement;\n } else if (angular.isDefined(duration)) {\n aliasFn = this.duScrollToAnimated;\n }\n if (aliasFn) {\n return aliasFn.apply(this, arguments);\n }\n var el = unwrap(this);\n if (isDocument(el)) {\n return $window.scrollTo(left, top);\n }\n el.scrollLeft = left;\n el.scrollTop = top;\n };\n\n var scrollAnimation, deferred;\n proto.duScrollToAnimated = function (left, top, duration, easing) {\n if (duration && !easing) {\n easing = duScrollEasing;\n }\n var startLeft = this.duScrollLeft(),\n startTop = this.duScrollTop(),\n deltaLeft = Math.round(left - startLeft),\n deltaTop = Math.round(top - startTop);\n\n var startTime = null, progress = 0;\n var el = this;\n\n var cancelScrollAnimation = function ($event) {\n if (!$event || (progress && $event.which > 0)) {\n if (duScrollCancelOnEvents) {\n el.unbind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n cancelAnimation(scrollAnimation);\n deferred.reject();\n scrollAnimation = null;\n }\n };\n\n if (scrollAnimation) {\n cancelScrollAnimation();\n }\n deferred = $q.defer();\n\n if (duration === 0 || (!deltaLeft && !deltaTop)) {\n if (duration === 0) {\n el.duScrollTo(left, top);\n }\n deferred.resolve();\n return deferred.promise;\n }\n\n var animationStep = function (timestamp) {\n if (startTime === null) {\n startTime = timestamp;\n }\n\n progress = timestamp - startTime;\n var percent = (progress >= duration ? 1 : easing(progress / duration));\n\n el.scrollTo(\n startLeft + Math.ceil(deltaLeft * percent),\n startTop + Math.ceil(deltaTop * percent)\n );\n if (percent < 1) {\n scrollAnimation = requestAnimation(animationStep);\n } else {\n if (duScrollCancelOnEvents) {\n el.unbind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n scrollAnimation = null;\n deferred.resolve();\n }\n };\n\n //Fix random mobile safari bug when scrolling to top by hitting status bar\n el.duScrollTo(startLeft, startTop);\n\n if (duScrollCancelOnEvents) {\n el.bind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n\n scrollAnimation = requestAnimation(animationStep);\n return deferred.promise;\n };\n\n proto.duScrollToElement = function (target, offset, duration, easing) {\n var el = unwrap(this);\n if (!angular.isNumber(offset) || isNaN(offset)) {\n offset = duScrollOffset;\n }\n var top = this.duScrollTop() + unwrap(target).getBoundingClientRect().top - offset;\n if (isElement(el)) {\n top -= el.getBoundingClientRect().top;\n }\n return this.duScrollTo(0, top, duration, easing);\n };\n\n proto.duScrollLeft = function (value, duration, easing) {\n if (angular.isNumber(value)) {\n return this.duScrollTo(value, this.duScrollTop(), duration, easing);\n }\n var el = unwrap(this);\n if (isDocument(el)) {\n return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft;\n }\n return el.scrollLeft;\n };\n proto.duScrollTop = function (value, duration, easing) {\n if (angular.isNumber(value)) {\n return this.duScrollTo(this.duScrollLeft(), value, duration, easing);\n }\n var el = unwrap(this);\n if (isDocument(el)) {\n return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;\n }\n return el.scrollTop;\n };\n\n proto.duScrollToElementAnimated = function (target, offset, duration, easing) {\n return this.duScrollToElement(target, offset, duration || duScrollDuration, easing);\n };\n\n proto.duScrollTopAnimated = function (top, duration, easing) {\n return this.duScrollTop(top, duration || duScrollDuration, easing);\n };\n\n proto.duScrollLeftAnimated = function (left, duration, easing) {\n return this.duScrollLeft(left, duration || duScrollDuration, easing);\n };\n\n angular.forEach(proto, function (fn, key) {\n angular.element.prototype[key] = fn;\n\n //Remove prefix if not already claimed by jQuery / ui.utils\n var unprefixed = key.replace(/^duScroll/, 'scroll');\n if (angular.isUndefined(angular.element.prototype[unprefixed])) {\n angular.element.prototype[unprefixed] = fn;\n }\n });\n\n }]);\n\n\n//Adapted from https://gist.github.com/paulirish/1579671\nangular.module('duScroll.polyfill', [])\n .factory('polyfill', [\"$window\", function ($window) {\n 'use strict';\n\n var vendors = ['webkit', 'moz', 'o', 'ms'];\n\n return function (fnName, fallback) {\n if ($window[fnName]) {\n return $window[fnName];\n }\n var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1);\n for (var key, i = 0; i < vendors.length; i++) {\n key = vendors[i] + suffix;\n if ($window[key]) {\n return $window[key];\n }\n }\n return fallback;\n };\n }]);\n\nangular.module('duScroll.requestAnimation', ['duScroll.polyfill'])\n .factory('requestAnimation', [\"polyfill\", \"$timeout\", function (polyfill, $timeout) {\n 'use strict';\n\n var lastTime = 0;\n var fallback = function (callback, element) {\n var currTime = new Date().getTime();\n var timeToCall = Math.max(0, 16 - (currTime - lastTime));\n var id = $timeout(function () {\n callback(currTime + timeToCall);\n },\n timeToCall);\n lastTime = currTime + timeToCall;\n return id;\n };\n\n return polyfill('requestAnimationFrame', fallback);\n }])\n .factory('cancelAnimation', [\"polyfill\", \"$timeout\", function (polyfill, $timeout) {\n 'use strict';\n\n var fallback = function (promise) {\n $timeout.cancel(promise);\n };\n\n return polyfill('cancelAnimationFrame', fallback);\n }]);\n\n\nangular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI'])\n .factory('spyAPI', [\"$rootScope\", \"$timeout\", \"$interval\", \"$window\", \"$document\", \"scrollContainerAPI\", \"duScrollGreedy\", \"duScrollSpyWait\", \"duScrollSpyRefreshInterval\", \"duScrollBottomSpy\", \"duScrollActiveClass\", function ($rootScope, $timeout, $interval, $window, $document, scrollContainerAPI, duScrollGreedy, duScrollSpyWait, duScrollSpyRefreshInterval, duScrollBottomSpy, duScrollActiveClass) {\n 'use strict';\n\n var createScrollHandler = function (context) {\n var timer = false, queued = false;\n var handler = function () {\n queued = false;\n var container = context.container,\n containerEl = container[0],\n containerOffset = 0,\n bottomReached;\n\n if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) {\n containerOffset = containerEl.getBoundingClientRect().top;\n bottomReached = Math.round(containerEl.scrollTop + containerEl.clientHeight) >= containerEl.scrollHeight;\n } else {\n var documentScrollHeight = $document[0].body.scrollHeight || $document[0].documentElement.scrollHeight; // documentElement for IE11\n bottomReached = Math.round($window.pageYOffset + $window.innerHeight) >= documentScrollHeight;\n }\n var compareProperty = (duScrollBottomSpy && bottomReached ? 'bottom' : 'top');\n\n var i, currentlyActive, toBeActive, spies, spy, pos;\n spies = context.spies;\n currentlyActive = context.currentlyActive;\n toBeActive = undefined;\n\n for (i = 0; i < spies.length; i++) {\n spy = spies[i];\n pos = spy.getTargetPosition();\n if (!pos || !spy.$element) continue;\n\n if ((duScrollBottomSpy && bottomReached) || (pos.top + spy.offset - containerOffset < 20 && (duScrollGreedy || pos.top * -1 + containerOffset) < pos.height)) {\n //Find the one closest the viewport top or the page bottom if it's reached\n if (!toBeActive || toBeActive[compareProperty] < pos[compareProperty]) {\n toBeActive = {\n spy: spy\n };\n toBeActive[compareProperty] = pos[compareProperty];\n }\n }\n }\n\n if (toBeActive) {\n toBeActive = toBeActive.spy;\n }\n if (currentlyActive === toBeActive || (duScrollGreedy && !toBeActive)) return;\n if (currentlyActive && currentlyActive.$element) {\n currentlyActive.$element.removeClass(duScrollActiveClass);\n $rootScope.$broadcast(\n 'duScrollspy:becameInactive',\n currentlyActive.$element,\n angular.element(currentlyActive.getTargetElement())\n );\n }\n if (toBeActive) {\n toBeActive.$element.addClass(duScrollActiveClass);\n $rootScope.$broadcast(\n 'duScrollspy:becameActive',\n toBeActive.$element,\n angular.element(toBeActive.getTargetElement())\n );\n }\n context.currentlyActive = toBeActive;\n };\n\n if (!duScrollSpyWait) {\n return handler;\n }\n\n //Debounce for potential performance savings\n return function () {\n if (!timer) {\n handler();\n timer = $timeout(function () {\n timer = false;\n if (queued) {\n handler();\n }\n }, duScrollSpyWait, false);\n } else {\n queued = true;\n }\n };\n };\n\n var contexts = {};\n\n var createContext = function ($scope) {\n var id = $scope.$id;\n var context = {\n spies: []\n };\n\n context.handler = createScrollHandler(context);\n contexts[id] = context;\n\n $scope.$on('$destroy', function () {\n destroyContext($scope);\n });\n\n return id;\n };\n\n var destroyContext = function ($scope) {\n var id = $scope.$id;\n var context = contexts[id], container = context.container;\n if (context.intervalPromise) {\n $interval.cancel(context.intervalPromise);\n }\n if (container) {\n container.off('scroll', context.handler);\n }\n delete contexts[id];\n };\n\n var defaultContextId = createContext($rootScope);\n\n var getContextForScope = function (scope) {\n if (contexts[scope.$id]) {\n return contexts[scope.$id];\n }\n if (scope.$parent) {\n return getContextForScope(scope.$parent);\n }\n return contexts[defaultContextId];\n };\n\n var getContextForSpy = function (spy) {\n var context, contextId, scope = spy.$scope;\n if (scope) {\n return getContextForScope(scope);\n }\n //No scope, most likely destroyed\n for (contextId in contexts) {\n context = contexts[contextId];\n if (context.spies.indexOf(spy) !== -1) {\n return context;\n }\n }\n };\n\n var isElementInDocument = function (element) {\n while (element.parentNode) {\n element = element.parentNode;\n if (element === document) {\n return true;\n }\n }\n return false;\n };\n\n var addSpy = function (spy) {\n var context = getContextForSpy(spy);\n if (!context) return;\n context.spies.push(spy);\n if (!context.container || !isElementInDocument(context.container)) {\n if (context.container) {\n context.container.off('scroll', context.handler);\n }\n context.container = scrollContainerAPI.getContainer(spy.$scope);\n if (duScrollSpyRefreshInterval && !context.intervalPromise) {\n context.intervalPromise = $interval(context.handler, duScrollSpyRefreshInterval, 0, false);\n }\n context.container.on('scroll', context.handler).triggerHandler('scroll');\n }\n };\n\n var removeSpy = function (spy) {\n var context = getContextForSpy(spy);\n if (spy === context.currentlyActive) {\n $rootScope.$broadcast('duScrollspy:becameInactive', context.currentlyActive.$element);\n context.currentlyActive = null;\n }\n var i = context.spies.indexOf(spy);\n if (i !== -1) {\n context.spies.splice(i, 1);\n }\n spy.$element = null;\n };\n\n return {\n addSpy: addSpy,\n removeSpy: removeSpy,\n createContext: createContext,\n destroyContext: destroyContext,\n getContextForScope: getContextForScope\n };\n }]);\n\n\nangular.module('duScroll.scrollContainerAPI', [])\n .factory('scrollContainerAPI', [\"$document\", function ($document) {\n 'use strict';\n\n var containers = {};\n\n var setContainer = function (scope, element) {\n var id = scope.$id;\n containers[id] = element;\n return id;\n };\n\n var getContainerId = function (scope) {\n if (containers[scope.$id]) {\n return scope.$id;\n }\n if (scope.$parent) {\n return getContainerId(scope.$parent);\n }\n\n };\n\n var getContainer = function (scope) {\n var id = getContainerId(scope);\n return id ? containers[id] : $document;\n };\n\n var removeContainer = function (scope) {\n var id = getContainerId(scope);\n if (id) {\n delete containers[id];\n }\n };\n\n return {\n getContainerId: getContainerId,\n getContainer: getContainer,\n setContainer: setContainer,\n removeContainer: removeContainer\n };\n }]);\n\n\nangular.module('duScroll.smoothScroll', ['duScroll.scrollHelpers', 'duScroll.scrollContainerAPI'])\n .directive('duSmoothScroll', [\"duScrollDuration\", \"duScrollOffset\", \"scrollContainerAPI\", function (duScrollDuration, duScrollOffset, scrollContainerAPI) {\n 'use strict';\n\n return {\n link: function ($scope, $element, $attr) {\n $element.on('click', function (e) {\n if ((!$attr.href || $attr.href.indexOf('#') === -1) && $attr.duSmoothScroll === '') return;\n\n var id = $attr.href ? $attr.href.replace(/.*(?=#[^\\s]+$)/, '').substring(1) : $attr.duSmoothScroll;\n\n var target = document.getElementById(id) || document.getElementsByName(id)[0];\n if (!target || !target.getBoundingClientRect) return;\n\n if (e.stopPropagation) e.stopPropagation();\n if (e.preventDefault) e.preventDefault();\n\n var offset = $attr.offset ? parseInt($attr.offset, 10) : duScrollOffset;\n var duration = $attr.duration ? parseInt($attr.duration, 10) : duScrollDuration;\n var container = scrollContainerAPI.getContainer($scope);\n\n container.duScrollToElement(\n angular.element(target),\n isNaN(offset) ? 0 : offset,\n isNaN(duration) ? 0 : duration\n );\n });\n }\n };\n }]);\n\n\nangular.module('duScroll.spyContext', ['duScroll.spyAPI'])\n .directive('duSpyContext', [\"spyAPI\", function (spyAPI) {\n 'use strict';\n\n return {\n restrict: 'A',\n scope: true,\n compile: function compile(tElement, tAttrs, transclude) {\n return {\n pre: function preLink($scope, iElement, iAttrs, controller) {\n spyAPI.createContext($scope);\n }\n };\n }\n };\n }]);\n\n\nangular.module('duScroll.scrollContainer', ['duScroll.scrollContainerAPI'])\n .directive('duScrollContainer', [\"scrollContainerAPI\", function (scrollContainerAPI) {\n 'use strict';\n\n return {\n restrict: 'A',\n scope: true,\n compile: function compile(tElement, tAttrs, transclude) {\n return {\n pre: function preLink($scope, iElement, iAttrs, controller) {\n iAttrs.$observe('duScrollContainer', function (element) {\n if (angular.isString(element)) {\n element = document.getElementById(element);\n }\n\n element = (angular.isElement(element) ? angular.element(element) : iElement);\n scrollContainerAPI.setContainer($scope, element);\n $scope.$on('$destroy', function () {\n scrollContainerAPI.removeContainer($scope);\n });\n });\n }\n };\n }\n };\n }]);\n\n\nangular.module('duScroll.scrollspy', ['duScroll.spyAPI'])\n .directive('duScrollspy', [\"spyAPI\", \"duScrollOffset\", \"$timeout\", \"$rootScope\", function (spyAPI, duScrollOffset, $timeout, $rootScope) {\n 'use strict';\n\n var Spy = function (targetElementOrId, $scope, $element, offset) {\n if (angular.isElement(targetElementOrId)) {\n this.target = targetElementOrId;\n } else if (angular.isString(targetElementOrId)) {\n this.targetId = targetElementOrId;\n }\n this.$scope = $scope;\n this.$element = $element;\n this.offset = offset;\n };\n\n Spy.prototype.getTargetElement = function () {\n if (!this.target && this.targetId) {\n this.target = document.getElementById(this.targetId) || document.getElementsByName(this.targetId)[0];\n }\n return this.target;\n };\n\n Spy.prototype.getTargetPosition = function () {\n var target = this.getTargetElement();\n if (target) {\n return target.getBoundingClientRect();\n }\n };\n\n Spy.prototype.flushTargetCache = function () {\n if (this.targetId) {\n this.target = undefined;\n }\n };\n\n return {\n link: function ($scope, $element, $attr) {\n var href = $attr.ngHref || $attr.href;\n var targetId;\n\n if (href && href.indexOf('#') !== -1) {\n targetId = href.replace(/.*(?=#[^\\s]+$)/, '').substring(1);\n } else if ($attr.duScrollspy) {\n targetId = $attr.duScrollspy;\n } else if ($attr.duSmoothScroll) {\n targetId = $attr.duSmoothScroll;\n }\n if (!targetId) return;\n\n // Run this in the next execution loop so that the scroll context has a chance\n // to initialize\n var timeoutPromise = $timeout(function () {\n var spy = new Spy(targetId, $scope, $element, -($attr.offset ? parseInt($attr.offset, 10) : duScrollOffset));\n spyAPI.addSpy(spy);\n\n $scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy));\n var deregisterOnStateChange = $rootScope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy));\n $scope.$on('$destroy', function () {\n spyAPI.removeSpy(spy);\n deregisterOnStateChange();\n });\n }, 0, false);\n $scope.$on('$destroy', function () {\n $timeout.cancel(timeoutPromise);\n });\n }\n };\n }]);\n"]} \ No newline at end of file diff --git a/core/src/main/resources/static/js/templates.js b/core/src/main/resources/static/js/templates.js index 4760a1371..3cc699e18 100644 --- a/core/src/main/resources/static/js/templates.js +++ b/core/src/main/resources/static/js/templates.js @@ -1,71 +1,73 @@ -angular.module('templates').run(['$templateCache', function($templateCache) {$templateCache.put('static/html/about.html','\n\n
              \n Written by TheOtherP for the community.
              \n \n
              \n

              Program info

              \n Version: {{simpleInfos.currentVersion}}
              \n \n Container version: {{simpleInfos.packageInfo.version}}
              \n Container release type: {{simpleInfos.packageInfo.releaseType}}
              \n Container author: {{simpleInfos.packageInfo.author}}\n
              \n \n
              \n

              Contact

              \n If you have a question or a feature request I\'d prefer you to create an issue on GitHub.
              \n You can visit the subreddit or join the Discord channel.
              \n If you absolutely must you can reach me via mail but I really prefer any of the other ways.\n

              \n Sources, bugs, enhancements: https://github.com/theotherp/nzbhydra2\n

              \n

              Donations

              \n You\'re welcome to donate:\n
                \n
              • Bitcoin via 1LPCUF9eKEXi58nHbxTbJyfxCJkcCXKzvm
              • \n
              • Regular money via PayPal to theotherp@posteo.net
              • \n
              • Via GitHub sponsors which involves a recurring donation similar to Patreon.
              • \n \n
              \n \n Thanks to the handful of people who\'ve already donated! I really appreciate the gesture.
              \n
              \n A special thanks go to\n
              \n \n
              \n and\n
              \n \n
              \n for sponsoring me.\n \n

              \n

              License

              \n Licensed under the Apache License, Version 2.0 (the "License");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n
              \n'); -$templateCache.put('static/html/bugreport.html','
              \r\n
              \r\n
              \r\n

              Bugreport / Debug infos

              \r\n
              \r\n
              \r\n So you found a bug? Ideally raise\r\n an issue on github. If you don\'t have an account create one ;-) I prefer GitHub issues for communication. Otherwise\r\n send me a mail.
              \r\n But please read this first:
              \r\n Don\'t just tell me what the problem is. If you just post an exception from the console or say "x does not\r\n work" I probably won\'t be willing or able to help. Remember you want something\r\n from\r\n me.
              \r\n
                \r\n
              • \r\n Tell me what you expect to happen and what actually happens\r\n
              • \r\n
              • \r\n If hydra doesn\'t even start, tell me your OS and how you start it.\r\n
              • \r\n
              • \r\n If the website looks weird tell me what browser you use. If you use a reverse proxy post your config\r\n and your base URL setting.\r\n
              • \r\n
              • \r\n If the GUI behaves strangely or doesn\'t react as it should check the browser console for errors.\r\n
              • \r\n
              \r\n Tell me anything that might help. If you do all that I will do my best to help you and improve NZBHydra.\r\n \r\n

              \r\n If possible provide the log and your settings. Here you can get anonymized versions of both to be\r\n posted:
              \r\n \r\n
              \r\n \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n
              \r\n \r\n
              \r\n \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n

              CPU usage

              \r\n
              \r\n
              \r\n \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n

              Debug SQL execution

              \r\n
              \r\n
              \r\n You may want to take a look at the settings to make sure there\'s nothing in there you wouldn\'t want me to\r\n see.\r\n


              \r\n You can use the input box below to execute any SQL query against the database. You will likely never need\r\n this but it allows me to ask you to execute a query when I try to solve a bug.\r\n
              \r\n \r\n \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n

              Misc

              \r\n
              \r\n \r\n
              \r\n\r\n
              \r\n'); -$templateCache.put('static/html/celebration-modal.html','\n\n\n\n\n\n'); -$templateCache.put('static/html/changelog-modal.html','\r\n\r\n\r\n'); -$templateCache.put('static/html/checker-state.html','\r\n\r\n\r\n'); -$templateCache.put('static/html/configure-in-modal.html','\n\n\n'); -$templateCache.put('static/html/custom-mapping-help.html','\n\n\n'); -$templateCache.put('static/html/dirPagination.tpl.html',''); -$templateCache.put('static/html/file-selection.html','\r\n\r\n\r\n\r\n'); -$templateCache.put('static/html/migration-modal.html','\n\n'); -$templateCache.put('static/html/modal.html','\r\n\r\n\r\n'); -$templateCache.put('static/html/news-modal.html','\n\n\n'); -$templateCache.put('static/html/restart-modal.html','\r\n'); -$templateCache.put('static/html/results-pagination.html',''); -$templateCache.put('static/html/search-history-details-modal.html','\r\n\r\n\r\n\r\n'); -$templateCache.put('static/html/search-searchhistory-dropdown.html',''); -$templateCache.put('static/html/search-state.html','\r\n\r\n\r\n'); -$templateCache.put('static/html/searchtemplate.html',''); -$templateCache.put('static/html/update-modal.html','\n\n\n\n'); -$templateCache.put('static/html/welcome-modal.html','\r\n\r\n\r\n'); -$templateCache.put('static/html/config/color-control.html','\n\n
              \n \n \n \n \n \n \n \n\n
              '); -$templateCache.put('static/html/config/downloader-config-box.html','\n\n\n \n \n \n'); -$templateCache.put('static/html/config/downloader-config.html','\n\n
              \n \n
              \n \n \n
              \n
              \n\n\n
              \n
              \n
              \n
              \n
              \n \n \n
              \n
              \n
              \n
              \n
              '); -$templateCache.put('static/html/config/indexer-config-box.html','\n\n\n \n \n \n'); -$templateCache.put('static/html/config/indexer-config-selection.html','\n\n\n \n \n'); -$templateCache.put('static/html/config/indexer-config.html','\n\n
              \n \n
              \n \n
              \n \n \n
              \n
              \n
              \n \n
              \n
              \n
              \n
              '); -$templateCache.put('static/html/config/recheck-all-caps.html','\n\n
              \n \n
              \n \n \n \n
              \n
              '); -$templateCache.put('static/html/dataTable/columnFilterBoolean.html','
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              '); -$templateCache.put('static/html/dataTable/columnFilterCheckboxes.html','
              \r\n\r\n \r\n
              \r\n\r\n
            • \r\n \r\n \r\n \r\n
            • \r\n\r\n\r\n\r\n'); -$templateCache.put('static/html/dataTable/columnFilterFreetext.html',''); -$templateCache.put('static/html/dataTable/columnFilterNumberRange.html','\r\n \r\n Min\r\n \r\n {{::addon}}\r\n \r\n\r\n \r\n Max\r\n \r\n {{::addon}}\r\n \r\n\r\n \r\n \r\n'); -$templateCache.put('static/html/dataTable/columnFilterOuter.html','
              \r\n \r\n \r\n
              \r\n\r\n\r\n\r\n'); -$templateCache.put('static/html/dataTable/columnFilterTime.html','\r\n

              \r\n After\r\n \r\n \r\n \r\n \r\n

              \r\n\r\n\r\n

              \r\n Before\r\n \r\n \r\n \r\n \r\n

              \r\n\r\n \r\n \r\n
              '); -$templateCache.put('static/html/dataTable/columnSortable.html','\r\n\r\n\r\n '); -$templateCache.put('static/html/directives/addable-nzb-modal.html','\r\n\r\n'); -$templateCache.put('static/html/directives/addable-nzb.html','\n
              \n
              \n'); -$templateCache.put('static/html/directives/addable-nzbs.html','\r\n \r\n'); -$templateCache.put('static/html/directives/backup.html','
              \r\n\r\n
              \r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n {{ file.loaded }} kB\r\n \r\n\r\n\r\n
              \r\n

              Existing backups

              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n
              FilenameCreatedRestore
              {{\r\n backup.filename }}{{ backup.creationDate | reformatDate }}\r\n
              \r\n
              \r\n
              \r\n'); -$templateCache.put('static/html/directives/cfg-form-entry.html','
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ cfg }}\r\n {{ cfg.name }}\r\n {{ help }}\r\n
              \r\n\r\n
              '); -$templateCache.put('static/html/directives/checks-footer.html','\n\n\n\n\n'); -$templateCache.put('static/html/directives/connection-test.html','\r\n \r\n \r\n'); -$templateCache.put('static/html/directives/download-nzbs-button.html','
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n
              '); -$templateCache.put('static/html/directives/download-nzbzip-button.html','
              \r\n \r\n
              '); -$templateCache.put('static/html/directives/downloader-status-footer.html','\n'); -$templateCache.put('static/html/directives/footer.html','\n\n
              \n
              \n\n'); -$templateCache.put('static/html/directives/indexer-input.html','
              \r\n \r\n \r\n
              \r\n
              \r\n \r\n Priority\r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n\r\n
              \r\n'); -$templateCache.put('static/html/directives/indexer-selection-button.html','\r\n\r\n
              \r\n \r\n \r\n \r\n
              '); -$templateCache.put('static/html/directives/indexer-state-switch.html','\r\n'); -$templateCache.put('static/html/directives/log.html','\r\n \r\n
              \r\n
              \r\n \r\n
              \r\n\r\n \r\n
              \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Time (newest first)LevelLoggerMessage
              {{::line["@timestamp"] | formatTimestamp}}\r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              {{line.logger_name | formatClassname}}\r\n
              \r\n {{::line.message}}\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n\r\n \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n        
              \r\n
              \r\n\r\n \r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n\r\n
              \r\n\r\n\r\n'); -$templateCache.put('static/html/directives/multiselect-dropdown.html','\n\n'); -$templateCache.put('static/html/directives/news.html','
              \n
              \n
              \n
              \n
              \n

              \n {{entry.version}}\n \n (This version)\n (Newer version)\n \n

              \n
              \n
              \n
              \n
              \n
              \n \n

              No news yet ;-)

              \n
              \n
              '); -$templateCache.put('static/html/directives/save-or-send-file.html','\r\n \r\n \r\n \r\n\r\n\r\n'); -$templateCache.put('static/html/directives/search-result.html','\n \n \n \n \n \n \n \n \n {{result.alwaysShowTitles}}\n \n \n \n {{ ::result.title }}\n \n \n {{::result.torrentDownloadFactor}}\n \n \n {{ ::result.indexer }}\n \n \n {{ ::result.category }}\n \n \n {{ ::result.size | byteFmt: 2 }}\n \n \n \n {{ ::result.grabs | kify }}\n \n \n /\n \n \n {{ ::result.seeders | kify }} / {{ ::result.peers | kify }}\n \n \n \n {{ ::result.age }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n'); -$templateCache.put('static/html/directives/selection-button.html','
              \r\n \r\n \r\n \r\n
              '); -$templateCache.put('static/html/directives/tab-or-chart.html','
              \r\n \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n
              '); -$templateCache.put('static/html/directives/tasks.html','\n\n
              \n\n
              \n

              Tasks

              \n \n \n \n \n \n \n \n \n \n \n \n
              NameLast executionNext execution
              {{task.name}}{{ task.lastExecutionTime | humanizeDate }}\n {{task.nextExecutionTime | humanizeDate}}\n
              \n
              \n
              '); -$templateCache.put('static/html/directives/updates.html','
              \n\n Current version: {{ currentVersion }}\n
              \n Latest version: {{ latestVersion }} Beta\n \n
              \n Latest beta version: {{ betaVersion }}\n
              \n
              \n \n
              \n
              \n A new release ({{ latestVersion }} Beta) is available.\n
              \n \n \n
              \n
              \n
              \n A new beta release ({{ betaVersion }}) is available.\n
              \n \n \n
              \n
              \n You\'re up to date!\n The latest version was ignored by you.\n \n\n
              \n
              \n \n
              \n\n \n
              \n'); -$templateCache.put('static/html/directives/version-history.html','\n\n
              \n
              \n

              Version history

              \n
              \n
            • \n

              {{::entry.version}} Beta ({{::entry.date}})

              \n
              \n \n Note\n Fix\n Feature\n \n \n
              \n
            • \n
              \n
              \n
              \n'); -$templateCache.put('static/html/states/config.html','
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n'); -$templateCache.put('static/html/states/download-history.html','
              \n
              \n
              \n \n
              \n
              \n \n
              \n
              \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
              \n Time\n \n \n \n \n \n Indexer\n \n \n \n \n \n Title\n \n \n \n \n \n Result \n \n \n \n \n \n Source\n \n \n \n \n \n Age\n \n \n \n \n Username\n \n \n \n \n Host\n \n \n \n
              {{ ::nzbDownload.time | reformatDate }}{{ ::nzbDownload.searchResult.indexer.name }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n {{ ::nzbDownload.searchResult.title }}{{ nzbDownload.searchResult.title }}\n \n \n \n {{ ::nzbDownload.accessSource === "INTERNAL" ? "Internal" : "API"}}{{ ::nzbDownload.age }}{{ ::nzbDownload.username }}{{ ::nzbDownload.ip }}
              \n\n\n'); -$templateCache.put('static/html/states/header.html',''); -$templateCache.put('static/html/states/indexer-statuses.html','\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Indexer statuses sorted by state, then name. Go to to the config to reenable any disabled indexers\r\n
              IndexerStateDisabled atDisabled untilLast error API hits Downloads Next hit allowed \r\n VIP expiry
              {{ indexerStatus.indexer }}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ indexerStatus.disabledAt | reformatDate}}\r\n \r\n {{ indexerStatus.disabledUntil | reformatDate}}\r\n {{ indexerStatus.lastError }}{{::indexerStatus.apiHits}}/{{::indexerStatus.apiHitLimit}}{{::indexerStatus.downloadHits}}/{{::indexerStatus.downloadHitLimit}}{{::indexerStatus.apiResetTime | formatTimestamp}}/{{::indexerStatus.downloadResetTime | formatTimestamp}}{{::indexerStatus.vipExpirationDate}}
              '); -$templateCache.put('static/html/states/logged-out.html','
              \n
              \n
              \n
              \n

              You were logged out

              \n
              \n
              \n
              \n
              \n'); -$templateCache.put('static/html/states/login.html','
              \n
              \n
              \n
              \n

              Log in

              \n
              \n
              \n \n \n
              \n
              \n \n \n
              \n \n You will be forwarded to the search area.\n
              \n
              \n
              \n
              \n
              \n'); -$templateCache.put('static/html/states/main-stats.html','\r\n \r\n\r\n
              \r\n
              \r\n Disclaimer: Don\'t read too much into these stats. Which indexer is picked for a download depends on its score\r\n and some more or less random values like posting time of the NZB.\r\n Some indexers might have nightly downtime which would influence the percentage of successful accesses.\r\n
              \r\n
              \r\n

              \r\n After\r\n \r\n \r\n \r\n \r\n

              \r\n
              \r\n
              \r\n

              \r\n Before\r\n \r\n \r\n \r\n \r\n

              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n Avg. response times (in ms) \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerAvg. response time (ms)Delta
              {{ avgResponseTime.indexer }}{{ avgResponseTime.avgResponseTime }}{{ avgResponseTime.delta }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Indexer scores\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerAvg. score \r\n # of dl searches Unique downloads
              {{ entry.indexerName }}{{ entry.averageUniquenessScore }}{{ entry.involvedSearches }}{{ entry.uniqueDownloads }}
              \r\n\r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n Indexer API accesses \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerAvg. per day% successful% failed
              {{ avgIndexerAccessSuccess.indexerName }}{{ avgIndexerAccessSuccess.averageAccessesPerDay | number: 0 }}{{ avgIndexerAccessSuccess.percentSuccessful | number: 0}}{{ avgIndexerAccessSuccess.percentConnectionError | number: 0 }}
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per indexer \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerTotal% of all enabled
              {{ indexerDownloads.indexerName }}{{ indexerDownloads.total | number: 0}}{{ indexerDownloads.share | number: 0 }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per age (in 100 day steps, all downloads)\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Average age{{ stats.downloadsPerAgeStats.averageAge}}
              % older than 1000 days{{ stats.downloadsPerAgeStats.percentOlder1000 | number : 1}}
              % older than 2000 days{{ stats.downloadsPerAgeStats.percentOlder2000 | number : 1}}
              % older than 3000 days{{ stats.downloadsPerAgeStats.percentOlder3000 | number : 1}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Successful downloads per indexer \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Indexer% of successful downloads# of all downloads# of successful downloads# of unsuccessful downloads
              {{ stat.indexerName}}{{ stat.percentSuccessful | number : 1}}{{ stat.countAll | number : 0}}{{ stat.countSuccessful | number : 0}}{{ stat.countError | number : 0}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n\r\n Searches per username\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              UserPercentageCount
              {{ stat.key }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Downloads per username\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              UserPercentageCount
              {{ stat.user }}{{ stat.percentage | number : 1}}{{ stat.count}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n Searches per host\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              HostPercentageCount
              {{ stat.key }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Downloads per host\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              HostPercentageCount
              {{ stat.key }}{{ stat.percentage | number : 1}}{{ stat.count}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n API Searches per user agent \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              User agentPercentageCount
              {{ stat.userAgent }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n API downloads per user agent \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              User agentPercentageCount
              {{ stat.userAgent }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n Searches per day of week\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Day of the weekSearches
              {{ stat.day }}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Searches per hour of day\r\n
              \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Hour of the daySearches
              {{ stat.hour }}{{ stat.count }}
              \r\n \r\n\r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per day of week\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Day of the weekDownloads
              {{ stat.day }}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per hour of day\r\n
              \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Hour of the dayDownloads
              {{ stat.hour }}{{ stat.count }}
              \r\n \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n'); -$templateCache.put('static/html/states/notification-history.html','
              \n
              \n
              \n
              \n \n
              \n
              \n \n
              \n
              \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
              \n Time\n \n \n \n \n \n Type\n \n \n \n \n \n Title\n \n Body\n \n URLs\n
              {{ notification.time | reformatDate }}{{ ::notification.title }}{{ ::notification.urls }}
              \n \n\n
              '); -$templateCache.put('static/html/states/search-history.html','
              \r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              \r\n Time\r\n \r\n \r\n \r\n \r\n \r\n Query\r\n \r\n \r\n \r\n \r\n \r\n Category\r\n \r\n \r\n \r\n \r\n \r\n Additional parameters\r\n \r\n Source\r\n \r\n \r\n \r\n \r\n \r\n User\r\n \r\n \r\n \r\n \r\n Host\r\n \r\n \r\n \r\n Details
              {{ request.time | reformatDate }}\r\n \r\n \r\n {{ formatQuery(request) }}\r\n \r\n {{ ::request.categoryName }}{{ ::request.source === "INTERNAL" ? "Internal" : "API"}}{{ ::request.username }}{{ ::request.ip }}\r\n
              \r\n \r\n\r\n
              '); -$templateCache.put('static/html/states/search-results.html','\n
              \n\n
              \n
              \n \n
              \n \n \n Indexer statuses / Rejected results \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n \n
              \n Indexer\n \n Results\n \n Response time\n \n Status\n
              \n {{ ::ps.indexerName }}\n \n \n >{{ ::ps.numberOfAvailableResults }}\n \n \n \n {{ ::ps.responseTime }}ms\n \n \n \n \n {{ ::ps.errorMessage }}\n \n\n Did not search.\n
              \n {{ ::ps.indexer }}\n \n \n \n \n {{::ps.reason}}\n
              \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
              \n \n Reject reason\n \n Count\n \n
              \n \n {{ entry[0] }}\n \n {{ entry[1] }}\n \n
              \n
              \n
              \n
              \n
              \n\n
              \n
              \n

              No indexers were picked for this search

              \n
              \n Reasons:\n
                \n
              • {{::tuple.indexer}}: {{::tuple.reason}}
              • \n
              \n
              \n
              \n
              \n\n
              \n
              \n

              Unable to search any indexer successfully; no results available

              \n

              No results were found for this search

              \n
              \n
              \n
              \n
              \n \n\n \n \n \n \n
              \n\n
              \n \n
              \n
              \n\n\n
              \n \n \n Loaded {{ numberOfLoadedResults }} ({{ numberOfFilteredResults }} filtered, {{numberOfDuplicateResults}} duplicates) of >{{ numberOfAvailableResults }} results (rejected {{ numberOfRejectedResults }})\n \n \n Loaded all {{ numberOfLoadedResults }} results (rejected {{ numberOfRejectedResults }})\n \n \n\n
              \n \n \n \n \n
              \n\n
              \n
              \n\n
              \n
              \n \n
              \n
              \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n {{result.title}}\n\n
              \n Title\n \n \n \n \n \n Indexer\n \n \n \n \n \n Category\n \n \n \n \n \n Size\n \n \n \n \n \n Details\n \n \n \n \n \n Age\n \n \n \n \n \n Links\n
              \n

              All results are currently filtered

              \n

              All found results have been rejected

              \n
              \n \n
              \n
              \n'); -$templateCache.put('static/html/states/search.html','\r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              \r\n \r\n \r\n {{selectedItem.title}}\r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n\r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n Min\r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n Max\r\n \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n Min\r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n Max\r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n \r\n
              '); -$templateCache.put('static/html/states/stats.html','\r\n\r\n
              \r\n'); -$templateCache.put('static/html/states/system.html','\n\n
              \n\n
              \n \n \n
              \n \n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n
              \n\n');}]); \ No newline at end of file +angular.module('templates').run(['$templateCache', function ($templateCache) { + $templateCache.put('static/html/about.html', '\n\n
              \n Written by TheOtherP for the community.
              \n \n
              \n

              Program info

              \n Version: {{simpleInfos.currentVersion}}
              \n \n Container version: {{simpleInfos.packageInfo.version}}
              \n Container release type: {{simpleInfos.packageInfo.releaseType}}
              \n Container author: {{simpleInfos.packageInfo.author}}\n
              \n \n
              \n

              Contact

              \n If you have a question or a feature request I\'d prefer you to create an issue on GitHub.
              \n You can visit the subreddit or join the Discord channel.
              \n If you absolutely must you can reach me via mail but I really prefer any of the other ways.\n

              \n Sources, bugs, enhancements: https://github.com/theotherp/nzbhydra2\n

              \n

              Donations

              \n You\'re welcome to donate:\n
                \n
              • Bitcoin via 1LPCUF9eKEXi58nHbxTbJyfxCJkcCXKzvm
              • \n
              • Regular money via PayPal to theotherp@posteo.net
              • \n
              • Via GitHub sponsors which involves a recurring donation similar to Patreon.
              • \n \n
              \n \n Thanks to the handful of people who\'ve already donated! I really appreciate the gesture.
              \n
              \n A special thanks go to\n
              \n \n
              \n and\n
              \n \n
              \n for sponsoring me.\n \n

              \n

              License

              \n Licensed under the Apache License, Version 2.0 (the "License");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n
              \n'); + $templateCache.put('static/html/bugreport.html', '
              \r\n
              \r\n
              \r\n

              Bugreport / Debug infos

              \r\n
              \r\n
              \r\n So you found a bug? Ideally raise\r\n an issue on github. If you don\'t have an account create one ;-) I prefer GitHub issues for communication. Otherwise\r\n send me a mail.
              \r\n But please read this first:
              \r\n Don\'t just tell me what the problem is. If you just post an exception from the console or say "x does not\r\n work" I probably won\'t be willing or able to help. Remember you want something\r\n from\r\n me.
              \r\n
                \r\n
              • \r\n Tell me what you expect to happen and what actually happens\r\n
              • \r\n
              • \r\n If hydra doesn\'t even start, tell me your OS and how you start it.\r\n
              • \r\n
              • \r\n If the website looks weird tell me what browser you use. If you use a reverse proxy post your config\r\n and your base URL setting.\r\n
              • \r\n
              • \r\n If the GUI behaves strangely or doesn\'t react as it should check the browser console for errors.\r\n
              • \r\n
              \r\n Tell me anything that might help. If you do all that I will do my best to help you and improve NZBHydra.\r\n \r\n

              \r\n If possible provide the log and your settings. Here you can get anonymized versions of both to be\r\n posted:
              \r\n \r\n
              \r\n \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n
              \r\n \r\n
              \r\n \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n

              CPU usage

              \r\n
              \r\n
              \r\n \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n

              Debug SQL execution

              \r\n
              \r\n
              \r\n You may want to take a look at the settings to make sure there\'s nothing in there you wouldn\'t want me to\r\n see.\r\n


              \r\n You can use the input box below to execute any SQL query against the database. You will likely never need\r\n this but it allows me to ask you to execute a query when I try to solve a bug.\r\n
              \r\n \r\n \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n

              Misc

              \r\n
              \r\n \r\n
              \r\n\r\n
              \r\n'); + $templateCache.put('static/html/celebration-modal.html', '\n\n\n\n\n\n'); + $templateCache.put('static/html/changelog-modal.html', '\r\n\r\n\r\n'); + $templateCache.put('static/html/checker-state.html', '\r\n\r\n\r\n'); + $templateCache.put('static/html/configure-in-modal.html', '\n\n\n'); + $templateCache.put('static/html/custom-mapping-help.html', '\n\n\n'); + $templateCache.put('static/html/dirPagination.tpl.html', ''); + $templateCache.put('static/html/file-selection.html', '\r\n\r\n\r\n\r\n'); + $templateCache.put('static/html/migration-modal.html', '\n\n'); + $templateCache.put('static/html/modal.html', '\r\n\r\n\r\n'); + $templateCache.put('static/html/news-modal.html', '\n\n\n'); + $templateCache.put('static/html/restart-modal.html', '\r\n'); + $templateCache.put('static/html/results-pagination.html', ''); + $templateCache.put('static/html/search-history-details-modal.html', '\r\n\r\n\r\n\r\n'); + $templateCache.put('static/html/search-searchhistory-dropdown.html', ''); + $templateCache.put('static/html/search-state.html', '\r\n\r\n\r\n'); + $templateCache.put('static/html/searchtemplate.html', ''); + $templateCache.put('static/html/update-modal.html', '\n\n\n\n'); + $templateCache.put('static/html/welcome-modal.html', '\r\n\r\n\r\n'); + $templateCache.put('static/html/config/color-control.html', '\n\n
              \n \n \n \n \n \n \n \n\n
              '); + $templateCache.put('static/html/config/downloader-config-box.html', '\n\n\n \n \n \n'); + $templateCache.put('static/html/config/downloader-config.html', '\n\n
              \n \n
              \n \n \n
              \n
              \n\n\n
              \n
              \n
              \n
              \n
              \n \n \n
              \n
              \n
              \n
              \n
              '); + $templateCache.put('static/html/config/indexer-config-box.html', '\n\n\n \n \n \n'); + $templateCache.put('static/html/config/indexer-config-selection.html', '\n\n\n \n \n'); + $templateCache.put('static/html/config/indexer-config.html', '\n\n
              \n \n
              \n \n
              \n \n \n
              \n
              \n
              \n \n
              \n
              \n
              \n
              '); + $templateCache.put('static/html/config/recheck-all-caps.html', '\n\n
              \n \n
              \n \n \n \n
              \n
              '); + $templateCache.put('static/html/dataTable/columnFilterBoolean.html', '
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              '); + $templateCache.put('static/html/dataTable/columnFilterCheckboxes.html', '
              \r\n\r\n \r\n
              \r\n\r\n
            • \r\n \r\n \r\n \r\n
            • \r\n\r\n\r\n\r\n'); + $templateCache.put('static/html/dataTable/columnFilterFreetext.html', ''); + $templateCache.put('static/html/dataTable/columnFilterNumberRange.html', '\r\n \r\n Min\r\n \r\n {{::addon}}\r\n \r\n\r\n \r\n Max\r\n \r\n {{::addon}}\r\n \r\n\r\n \r\n \r\n'); + $templateCache.put('static/html/dataTable/columnFilterOuter.html', '
              \r\n \r\n \r\n
              \r\n\r\n\r\n\r\n'); + $templateCache.put('static/html/dataTable/columnFilterTime.html', '\r\n

              \r\n After\r\n \r\n \r\n \r\n \r\n

              \r\n\r\n\r\n

              \r\n Before\r\n \r\n \r\n \r\n \r\n

              \r\n\r\n \r\n \r\n
              '); + $templateCache.put('static/html/dataTable/columnSortable.html', '\r\n\r\n\r\n '); + $templateCache.put('static/html/directives/addable-nzb-modal.html', '\r\n\r\n'); + $templateCache.put('static/html/directives/addable-nzb.html', '\n
              \n
              \n'); + $templateCache.put('static/html/directives/addable-nzbs.html', '\r\n \r\n'); + $templateCache.put('static/html/directives/backup.html', '
              \r\n\r\n
              \r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n {{ file.loaded }} kB\r\n \r\n\r\n\r\n
              \r\n

              Existing backups

              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n
              FilenameCreatedRestore
              {{\r\n backup.filename }}{{ backup.creationDate | reformatDate }}\r\n
              \r\n
              \r\n
              \r\n'); + $templateCache.put('static/html/directives/cfg-form-entry.html', '
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ cfg }}\r\n {{ cfg.name }}\r\n {{ help }}\r\n
              \r\n\r\n
              '); + $templateCache.put('static/html/directives/checks-footer.html', '\n\n\n\n\n'); + $templateCache.put('static/html/directives/connection-test.html', '\r\n \r\n \r\n'); + $templateCache.put('static/html/directives/download-nzbs-button.html', '
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n
              '); + $templateCache.put('static/html/directives/download-nzbzip-button.html', '
              \r\n \r\n
              '); + $templateCache.put('static/html/directives/downloader-status-footer.html', '\n'); + $templateCache.put('static/html/directives/footer.html', '\n\n
              \n
              \n\n'); + $templateCache.put('static/html/directives/indexer-input.html', '
              \r\n \r\n \r\n
              \r\n
              \r\n \r\n Priority\r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n\r\n
              \r\n'); + $templateCache.put('static/html/directives/indexer-selection-button.html', '\r\n\r\n
              \r\n \r\n \r\n \r\n
              '); + $templateCache.put('static/html/directives/indexer-state-switch.html', '\r\n'); + $templateCache.put('static/html/directives/log.html', '\r\n \r\n
              \r\n
              \r\n \r\n
              \r\n\r\n \r\n
              \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Time (newest first)LevelLoggerMessage
              {{::line["@timestamp"] | formatTimestamp}}\r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              {{line.logger_name | formatClassname}}\r\n
              \r\n {{::line.message}}\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n\r\n \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n        
              \r\n
              \r\n\r\n \r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n\r\n
              \r\n\r\n\r\n'); + $templateCache.put('static/html/directives/multiselect-dropdown.html', '\n\n'); + $templateCache.put('static/html/directives/news.html', '
              \n
              \n
              \n
              \n
              \n

              \n {{entry.version}}\n \n (This version)\n (Newer version)\n \n

              \n
              \n
              \n
              \n
              \n
              \n \n

              No news yet ;-)

              \n
              \n
              '); + $templateCache.put('static/html/directives/save-or-send-file.html', '\r\n \r\n \r\n \r\n\r\n\r\n'); + $templateCache.put('static/html/directives/search-result.html', '\n \n \n \n \n \n \n \n \n {{result.alwaysShowTitles}}\n \n \n \n {{ ::result.title }}\n \n \n {{::result.torrentDownloadFactor}}\n \n \n {{ ::result.indexer }}\n \n \n {{ ::result.category }}\n \n \n {{ ::result.size | byteFmt: 2 }}\n \n \n \n {{ ::result.grabs | kify }}\n \n \n /\n \n \n {{ ::result.seeders | kify }} / {{ ::result.peers | kify }}\n \n \n \n {{ ::result.age }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n'); + $templateCache.put('static/html/directives/selection-button.html', '
              \r\n \r\n \r\n \r\n
              '); + $templateCache.put('static/html/directives/tab-or-chart.html', '
              \r\n \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n
              '); + $templateCache.put('static/html/directives/tasks.html', '\n\n
              \n\n
              \n

              Tasks

              \n \n \n \n \n \n \n \n \n \n \n \n
              NameLast executionNext execution
              {{task.name}}{{ task.lastExecutionTime | humanizeDate }}\n {{task.nextExecutionTime | humanizeDate}}\n
              \n
              \n
              '); + $templateCache.put('static/html/directives/updates.html', '
              \n\n Current version: {{ currentVersion }}\n
              \n Latest version: {{ latestVersion }} Beta\n \n
              \n Latest beta version: {{ betaVersion }}\n
              \n
              \n \n
              \n
              \n A new release ({{ latestVersion }} Beta) is available.\n
              \n \n \n
              \n
              \n
              \n A new beta release ({{ betaVersion }}) is available.\n
              \n \n \n
              \n
              \n You\'re up to date!\n The latest version was ignored by you.\n \n\n
              \n
              \n \n
              \n\n \n
              \n'); + $templateCache.put('static/html/directives/version-history.html', '\n\n
              \n
              \n

              Version history

              \n
              \n
            • \n

              {{::entry.version}} Beta ({{::entry.date}})

              \n
              \n \n Note\n Fix\n Feature\n \n \n
              \n
            • \n
              \n
              \n
              \n'); + $templateCache.put('static/html/states/config.html', '
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n'); + $templateCache.put('static/html/states/download-history.html', '
              \n
              \n
              \n \n
              \n
              \n \n
              \n
              \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
              \n Time\n \n \n \n \n \n Indexer\n \n \n \n \n \n Title\n \n \n \n \n \n Result \n \n \n \n \n \n Source\n \n \n \n \n \n Age\n \n \n \n \n Username\n \n \n \n \n Host\n \n \n \n
              {{ ::nzbDownload.time | reformatDate }}{{ ::nzbDownload.searchResult.indexer.name }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n {{ ::nzbDownload.searchResult.title }}{{ nzbDownload.searchResult.title }}\n \n \n \n {{ ::nzbDownload.accessSource === "INTERNAL" ? "Internal" : "API"}}{{ ::nzbDownload.age }}{{ ::nzbDownload.username }}{{ ::nzbDownload.ip }}
              \n\n\n'); + $templateCache.put('static/html/states/header.html', ''); + $templateCache.put('static/html/states/indexer-statuses.html', '\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Indexer statuses sorted by state, then name. Go to to the config to reenable any disabled indexers\r\n
              IndexerStateDisabled atDisabled untilLast error API hits Downloads Next hit allowed \r\n VIP expiry
              {{ indexerStatus.indexer }}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {{ indexerStatus.disabledAt | reformatDate}}\r\n \r\n {{ indexerStatus.disabledUntil | reformatDate}}\r\n {{ indexerStatus.lastError }}{{::indexerStatus.apiHits}}/{{::indexerStatus.apiHitLimit}}{{::indexerStatus.downloadHits}}/{{::indexerStatus.downloadHitLimit}}{{::indexerStatus.apiResetTime | formatTimestamp}}/{{::indexerStatus.downloadResetTime | formatTimestamp}}{{::indexerStatus.vipExpirationDate}}
              '); + $templateCache.put('static/html/states/logged-out.html', '
              \n
              \n
              \n
              \n

              You were logged out

              \n
              \n
              \n
              \n
              \n'); + $templateCache.put('static/html/states/login.html', '
              \n
              \n
              \n
              \n

              Log in

              \n
              \n
              \n \n \n
              \n
              \n \n \n
              \n \n You will be forwarded to the search area.\n
              \n
              \n
              \n
              \n
              \n'); + $templateCache.put('static/html/states/main-stats.html', '\r\n \r\n\r\n
              \r\n
              \r\n Disclaimer: Don\'t read too much into these stats. Which indexer is picked for a download depends on its score\r\n and some more or less random values like posting time of the NZB.\r\n Some indexers might have nightly downtime which would influence the percentage of successful accesses.\r\n
              \r\n
              \r\n

              \r\n After\r\n \r\n \r\n \r\n \r\n

              \r\n
              \r\n
              \r\n

              \r\n Before\r\n \r\n \r\n \r\n \r\n

              \r\n
              \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n Avg. response times (in ms) \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerAvg. response time (ms)Delta
              {{ avgResponseTime.indexer }}{{ avgResponseTime.avgResponseTime }}{{ avgResponseTime.delta }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Indexer scores\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerAvg. score \r\n # of dl searches Unique downloads
              {{ entry.indexerName }}{{ entry.averageUniquenessScore }}{{ entry.involvedSearches }}{{ entry.uniqueDownloads }}
              \r\n\r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n Indexer API accesses \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerAvg. per day% successful% failed
              {{ avgIndexerAccessSuccess.indexerName }}{{ avgIndexerAccessSuccess.averageAccessesPerDay | number: 0 }}{{ avgIndexerAccessSuccess.percentSuccessful | number: 0}}{{ avgIndexerAccessSuccess.percentConnectionError | number: 0 }}
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per indexer \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              IndexerTotal% of all enabled
              {{ indexerDownloads.indexerName }}{{ indexerDownloads.total | number: 0}}{{ indexerDownloads.share | number: 0 }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per age (in 100 day steps, all downloads)\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Average age{{ stats.downloadsPerAgeStats.averageAge}}
              % older than 1000 days{{ stats.downloadsPerAgeStats.percentOlder1000 | number : 1}}
              % older than 2000 days{{ stats.downloadsPerAgeStats.percentOlder2000 | number : 1}}
              % older than 3000 days{{ stats.downloadsPerAgeStats.percentOlder3000 | number : 1}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Successful downloads per indexer \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Indexer% of successful downloads# of all downloads# of successful downloads# of unsuccessful downloads
              {{ stat.indexerName}}{{ stat.percentSuccessful | number : 1}}{{ stat.countAll | number : 0}}{{ stat.countSuccessful | number : 0}}{{ stat.countError | number : 0}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n\r\n Searches per username\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              UserPercentageCount
              {{ stat.key }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Downloads per username\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              UserPercentageCount
              {{ stat.user }}{{ stat.percentage | number : 1}}{{ stat.count}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n Searches per host\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              HostPercentageCount
              {{ stat.key }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Downloads per host\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              HostPercentageCount
              {{ stat.key }}{{ stat.percentage | number : 1}}{{ stat.count}}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n API Searches per user agent \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              User agentPercentageCount
              {{ stat.userAgent }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n API downloads per user agent \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              User agentPercentageCount
              {{ stat.userAgent }}{{ stat.percentage | number : 1}}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n Searches per day of week\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Day of the weekSearches
              {{ stat.day }}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n Searches per hour of day\r\n
              \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Hour of the daySearches
              {{ stat.hour }}{{ stat.count }}
              \r\n \r\n\r\n \r\n \r\n \r\n
              \r\n\r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per day of week\r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Day of the weekDownloads
              {{ stat.day }}{{ stat.count }}
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n NZB downloads per hour of day\r\n
              \r\n \r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              Hour of the dayDownloads
              {{ stat.hour }}{{ stat.count }}
              \r\n \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n\r\n
              \r\n'); + $templateCache.put('static/html/states/notification-history.html', '
              \n
              \n
              \n
              \n \n
              \n
              \n \n
              \n
              \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
              \n Time\n \n \n \n \n \n Type\n \n \n \n \n \n Title\n \n Body\n \n URLs\n
              {{ notification.time | reformatDate }}{{ ::notification.title }}{{ ::notification.urls }}
              \n \n\n
              \n'); + $templateCache.put('static/html/states/search-history.html', '
              \r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              \r\n Time\r\n \r\n \r\n \r\n \r\n \r\n Query\r\n \r\n \r\n \r\n \r\n \r\n Category\r\n \r\n \r\n \r\n \r\n \r\n Additional parameters\r\n \r\n Source\r\n \r\n \r\n \r\n \r\n \r\n User\r\n \r\n \r\n \r\n \r\n Host\r\n \r\n \r\n \r\n Details
              {{ request.time | reformatDate }}\r\n \r\n \r\n {{ formatQuery(request) }}\r\n \r\n {{ ::request.categoryName }}{{ ::request.source === "INTERNAL" ? "Internal" : "API"}}{{ ::request.username }}{{ ::request.ip }}\r\n
              \r\n \r\n\r\n
              '); + $templateCache.put('static/html/states/search-results.html', '\n
              \n\n
              \n
              \n \n
              \n \n \n Indexer statuses / Rejected results \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n \n
              \n Indexer\n \n Results\n \n Response time\n \n Status\n
              \n {{ ::ps.indexerName }}\n \n \n >{{ ::ps.numberOfAvailableResults }}\n \n \n \n {{ ::ps.responseTime }}ms\n \n \n \n \n {{ ::ps.errorMessage }}\n \n\n Did not search.\n
              \n {{ ::ps.indexer }}\n \n \n \n \n {{::ps.reason}}\n
              \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
              \n \n Reject reason\n \n Count\n \n
              \n \n {{ entry[0] }}\n \n {{ entry[1] }}\n \n
              \n
              \n
              \n
              \n
              \n\n
              \n
              \n

              No indexers were picked for this search

              \n
              \n Reasons:\n
                \n
              • {{::tuple.indexer}}: {{::tuple.reason}}
              • \n
              \n
              \n
              \n
              \n\n
              \n
              \n

              Unable to search any indexer successfully; no results available

              \n

              No results were found for this search

              \n
              \n
              \n
              \n
              \n \n\n \n \n \n \n
              \n\n
              \n \n
              \n
              \n\n\n
              \n \n \n Loaded {{ numberOfLoadedResults }} ({{ numberOfFilteredResults }} filtered, {{numberOfDuplicateResults}} duplicates) of >{{ numberOfAvailableResults }} results (rejected {{ numberOfRejectedResults }})\n \n \n Loaded all {{ numberOfLoadedResults }} results (rejected {{ numberOfRejectedResults }})\n \n \n\n
              \n \n \n \n \n
              \n\n
              \n
              \n\n
              \n
              \n \n
              \n
              \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n {{result.title}}\n\n
              \n Title\n \n \n \n \n \n Indexer\n \n \n \n \n \n Category\n \n \n \n \n \n Size\n \n \n \n \n \n Details\n \n \n \n \n \n Age\n \n \n \n \n \n Links\n
              \n

              All results are currently filtered

              \n

              All found results have been rejected

              \n
              \n \n
              \n
              \n'); + $templateCache.put('static/html/states/search.html', '\r\n\r\n
              \r\n
              \r\n
              \r\n \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n\r\n \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n\r\n
              \r\n \r\n \r\n {{selectedItem.title}}\r\n \r\n
              \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
              \r\n \r\n \r\n
              \r\n
              \r\n\r\n \r\n \r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n\r\n \r\n
              \r\n
              \r\n
              \r\n \r\n\r\n
              \r\n
              \r\n \r\n \r\n \r\n \r\n\r\n
              \r\n
              \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n
              \r\n
              \r\n \r\n\r\n
              \r\n
              \r\n Min\r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n Max\r\n \r\n
              \r\n
              \r\n \r\n\r\n
              \r\n
              \r\n Min\r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n Max\r\n \r\n
              \r\n
              \r\n
              \r\n
              \r\n\r\n
              \r\n\r\n
              \r\n
              \r\n\r\n \r\n
              \r\n'); + $templateCache.put('static/html/states/stats.html', '\r\n\r\n
              \r\n'); + $templateCache.put('static/html/states/system.html', '\n\n
              \n\n
              \n \n \n
              \n \n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n\n
              \n \n
              \n
              \n\n'); +}]); diff --git a/core/src/main/resources/templates/error.html b/core/src/main/resources/templates/error.html index 7db36878e..755df1865 100644 --- a/core/src/main/resources/templates/error.html +++ b/core/src/main/resources/templates/error.html @@ -31,11 +31,19 @@ An error occurred. Check the logs. - + - + + + + + + + + + @@ -45,10 +53,6 @@ An error occurred. Check the logs. - - - -
              Path
              Query parameters
              Status
              Timestamp
              ErrorException
              Timestamp
              diff --git a/core/src/main/resources/wrapperHashes.json b/core/src/main/resources/wrapperHashes.json deleted file mode 100644 index 9452eafc5..000000000 --- a/core/src/main/resources/wrapperHashes.json +++ /dev/null @@ -1 +0,0 @@ -{"nzbhydra2wrapper.py":"53adb81fafa76547c54aa15ded071f2c97b5368a","nzbhydra2wrapperPy3.py":"408066a0f5811fdaf47b7a5cb975bcbc30ca9f82","NZBHydra2.exe":"c4830dea60bf45eeb58ba988c1bb11cc48fa8b34","NZBHydra2 Console.exe":"f0ae574a26205b9a912ca0f6b71f0281ff52c450","nzbhydra2":"cafab8ccc6548bb75429c1a9b92bfd6337ac4040"} \ No newline at end of file diff --git a/core/src/main/resources/wrapperHashes2.json b/core/src/main/resources/wrapperHashes2.json new file mode 100644 index 000000000..4d6129844 --- /dev/null +++ b/core/src/main/resources/wrapperHashes2.json @@ -0,0 +1,7 @@ +{ + "nzbhydra2wrapper.py": "1b1c589959312bc0d93508dd7793a2c15d66c9ab", + "nzbhydra2wrapperPy3.py": "678f5795c2130b38e45608e650ea08f32b1b9d1d", + "NZBHydra2.exe": "83ea6a154bcf07207b41e71bbd1ad91878e97c83", + "NZBHydra2 Console.exe": "eb161d5ee8a79988937bd3211dfd936e0d2a35e5", + "nzbhydra2": "701630899e7057a981ee21d75d2e048b476f0d7f" +} diff --git a/core/src/test/java/org/nzbhydra/Experiments.java b/core/src/test/java/org/nzbhydra/Experiments.java index ef7bfa863..38a84771a 100644 --- a/core/src/test/java/org/nzbhydra/Experiments.java +++ b/core/src/test/java/org/nzbhydra/Experiments.java @@ -16,7 +16,6 @@ package org.nzbhydra; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; @@ -26,23 +25,16 @@ import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import okhttp3.ResponseBody; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.indexer.IndexerCategoryConfig; import org.nzbhydra.config.indexer.IndexerConfig; -import org.nzbhydra.mapping.changelog.ChangelogVersionEntry; -import org.nzbhydra.mapping.github.Release; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -53,8 +45,8 @@ import java.util.stream.IntStream; public class Experiments { @Test - @Ignore - public void bla() throws IOException, InterruptedException { + @Disabled + void bla() throws IOException, InterruptedException { for (int i = 0; i < 1000; i++) { Call call = new OkHttpClient.Builder().build().newCall(new Request.Builder().url("http://127.0.0.1:5076/api?apikey=apikey&t=search&q=blade%20runner").build()); call.execute(); @@ -62,46 +54,10 @@ public class Experiments { } } - @Test - @Ignore - public void updateChangelogDates() throws Exception { - File jsonFile = new File("..\\core\\src\\main\\resources\\changelog.json"); - OkHttpClient client = new OkHttpClient.Builder().build(); - Map releaseDates = new HashMap<>(); - for (int i = 1; i <= 6; i++) { - Response response = client.newCall(new Request.Builder().url("https://api.github.com/repos/theotherp/nzbhydra2/releases?page=" + i).build()).execute(); - - ResponseBody body = response.body(); - List entries = Jackson.JSON_MAPPER.readValue(body.string(), new TypeReference>() { - }); - for (Release entry : entries) { - releaseDates.put(entry.getTagName(), entry.getPublishedAt().substring(0, 10)); - } - Thread.sleep(250); - } - - List changelogEntries = Jackson.JSON_MAPPER.readValue(jsonFile, new TypeReference>() { - }); - - List updatedChangelogEntries = new ArrayList<>(); - for (ChangelogVersionEntry versionEntry : changelogEntries) { - if (releaseDates.containsKey(versionEntry.getVersion())) { - versionEntry.setDate(releaseDates.get(versionEntry.getVersion())); - } - updatedChangelogEntries.add(versionEntry); - } - - Collections.sort(updatedChangelogEntries); - Collections.reverse(updatedChangelogEntries); - - Jackson.JSON_MAPPER.writeValue(jsonFile, updatedChangelogEntries); - - System.out.println(); - } @Test - @Ignore - public void createSimpleYaml() throws IOException, InterruptedException { + @Disabled + void createSimpleYaml() throws IOException, InterruptedException { ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); objectMapper.registerModule(new Jdk8Module()); @@ -117,8 +73,8 @@ public class Experiments { } @Test - @Ignore - public void createTestYaml() throws IOException, InterruptedException { + @Disabled + void createTestYaml() throws IOException, InterruptedException { ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); MainClass mainClass = new MainClass(); @@ -132,11 +88,11 @@ public class Experiments { } @Test - @Ignore - public void stressTest() throws Exception { + @Disabled + void stressTest() throws Exception { OkHttpClient client = new OkHttpClient.Builder() - .build(); + .build(); final int limit = 315; final Stopwatch started = Stopwatch.createStarted(); diff --git a/core/src/test/java/org/nzbhydra/NativeHintsTest.java b/core/src/test/java/org/nzbhydra/NativeHintsTest.java new file mode 100644 index 000000000..6fed6ce64 --- /dev/null +++ b/core/src/test/java/org/nzbhydra/NativeHintsTest.java @@ -0,0 +1,66 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra; + +import lombok.Data; +import org.apache.commons.io.FileUtils; +import org.assertj.core.api.Assertions; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster.model.source.JavaClassSource; +import org.jboss.forge.roaster.model.source.JavaSource; +import org.junit.jupiter.api.Test; +import org.nzbhydra.springnative.ReflectionMarker; + +import java.io.File; +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings("unchecked") +public class NativeHintsTest { + + @Test + public void ensureAllDataAnnotatedClassesAreMarked() throws Exception { + final Collection javaFiles = FileUtils.listFiles(new File(""), new String[]{"java"}, true); + javaFiles.addAll(FileUtils.listFiles(new File("../shared"), new String[]{"java"}, true)); + final Set classesWithMissingReflectionMarker = new HashSet<>(); + for (File javaFile : javaFiles) { + final JavaSource source; + source = Roaster.parse(JavaSource.class, javaFile); + addIfMarkerIsMissing(classesWithMissingReflectionMarker, source); + if (source instanceof JavaClassSource javaClassSource) { + addIfMarkerIsMissing(classesWithMissingReflectionMarker, javaClassSource); + } + } + Assertions.assertThat(classesWithMissingReflectionMarker).isEmpty(); + + } + + private static void addIfMarkerIsMissing(Set classesWithMissingReflectionMarker, JavaSource source) { + if (source.hasAnnotation(Data.class) && !source.hasAnnotation(ReflectionMarker.class)) { + classesWithMissingReflectionMarker.add(source.getCanonicalName()); + } + if (source instanceof JavaClassSource javaClassSource) { + if (javaClassSource.hasInterface(Serializable.class) && !source.hasAnnotation(ReflectionMarker.class)) { + classesWithMissingReflectionMarker.add(source.getCanonicalName()); + } + } + + } + +} diff --git a/core/src/test/java/org/nzbhydra/NzbHandlerTest.java b/core/src/test/java/org/nzbhydra/NzbHandlerTest.java index 7afe19c16..286b9a254 100644 --- a/core/src/test/java/org/nzbhydra/NzbHandlerTest.java +++ b/core/src/test/java/org/nzbhydra/NzbHandlerTest.java @@ -1,6 +1,6 @@ package org.nzbhydra; -import org.junit.Before; +import org.junit.jupiter.api.BeforeEach; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -10,8 +10,8 @@ import org.nzbhydra.downloading.FileDownloadEntity; import org.nzbhydra.downloading.FileDownloadRepository; import org.nzbhydra.downloading.FileHandler; -import static org.mockito.Mockito.when; - +import static org.mockito.Mockito.when; + public class NzbHandlerTest { @InjectMocks @@ -24,7 +24,7 @@ public class NzbHandlerTest { private FileDownloadEntity entityMock; private BaseConfig baseConfig = new BaseConfig(); - @Before + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); when(configProviderMock.getBaseConfig()).thenReturn(baseConfig); diff --git a/core/src/test/java/org/nzbhydra/api/CapsGeneratorTest.java b/core/src/test/java/org/nzbhydra/api/CapsGeneratorTest.java index f1c252965..684cac52d 100644 --- a/core/src/test/java/org/nzbhydra/api/CapsGeneratorTest.java +++ b/core/src/test/java/org/nzbhydra/api/CapsGeneratorTest.java @@ -16,8 +16,8 @@ package org.nzbhydra.api; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -40,7 +40,7 @@ public class CapsGeneratorTest { @InjectMocks private CapsGenerator testee = new CapsGenerator(); - @Before + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); BaseConfig baseConfig = new BaseConfig(); @@ -70,7 +70,7 @@ public class CapsGeneratorTest { } @Test - public void shouldGenerateCategoriesFromConfig() throws Exception { + void shouldGenerateCategoriesFromConfig() throws Exception { CapsXmlCategories xmlCategories = testee.getCapsXmlCategories(); assertThat(xmlCategories.getCategories().size()).isEqualTo(3); diff --git a/core/src/test/java/org/nzbhydra/api/CategoryConverterTest.java b/core/src/test/java/org/nzbhydra/api/CategoryConverterTest.java index c4a585125..f012e5760 100644 --- a/core/src/test/java/org/nzbhydra/api/CategoryConverterTest.java +++ b/core/src/test/java/org/nzbhydra/api/CategoryConverterTest.java @@ -1,15 +1,14 @@ package org.nzbhydra.api; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.category.Category; import org.nzbhydra.searching.CategoryProvider; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; public class CategoryConverterTest { @@ -20,7 +19,7 @@ public class CategoryConverterTest { @Mock private CategoryProvider categoryProviderMock; - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); testee.setCategoryProvider(categoryProviderMock); @@ -28,18 +27,18 @@ public class CategoryConverterTest { @Test - public void convertToDatabaseColumn() throws Exception { + void convertToDatabaseColumn() throws Exception { Category category = new Category(); category.setName("name"); - assertThat(testee.convertToDatabaseColumn(category), is("name")); + assertThat(testee.convertToDatabaseColumn(category)).isEqualTo("name"); } @Test - public void convertToEntityAttribute() throws Exception { + void convertToEntityAttribute() throws Exception { Category category = new Category(); when(categoryProviderMock.getByInternalName("name")).thenReturn(category); - assertThat(testee.convertToEntityAttribute("name"), is(category)); + assertThat(testee.convertToEntityAttribute("name")).isEqualTo(category); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/api/ExternalApiTest.java b/core/src/test/java/org/nzbhydra/api/ExternalApiTest.java index 82786261e..dcab320ea 100644 --- a/core/src/test/java/org/nzbhydra/api/ExternalApiTest.java +++ b/core/src/test/java/org/nzbhydra/api/ExternalApiTest.java @@ -1,7 +1,7 @@ package org.nzbhydra.api; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -10,7 +10,9 @@ import org.mockito.stubbing.Answer; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.MainConfig; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.downloading.FileHandler; import org.nzbhydra.indexers.Indexer; import org.nzbhydra.mapping.newznab.ActionAttribute; @@ -20,11 +22,10 @@ import org.nzbhydra.mapping.newznab.json.NewznabJsonRoot; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; import org.nzbhydra.misc.UserAgentMapper; import org.nzbhydra.searching.CategoryProvider; +import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; import org.nzbhydra.searching.SearchResult; import org.nzbhydra.searching.Searcher; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.nzbhydra.searching.searchrequests.SearchRequestFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -36,7 +37,6 @@ import java.time.ZoneId; import java.time.temporal.ChronoUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -69,10 +69,12 @@ public class ExternalApiTest { private NewznabJsonTransformer newznabJsonTransformerMock; @Mock private Jaxb2Marshaller jaxb2MarshallerMock; + @Mock + private CustomQueryAndTitleMappingHandler customQueryAndTitleMappingHandler; IndexerConfig indexerConfig = new IndexerConfig(); - @Before + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); when(configProvider.getBaseConfig()).thenReturn(baseConfig); @@ -97,11 +99,12 @@ public class ExternalApiTest { }).when(jaxb2MarshallerMock).marshal(any(), any()); when(indexerMock.getConfig()).thenReturn(indexerConfig); - when(newznabXmlTransformerMock.getRssRoot(any(), anyInt(), anyInt(), any())).thenReturn(new NewznabXmlRoot()); + when(newznabXmlTransformerMock.getRssRoot(any(), anyInt(), anyInt(), any(Boolean.class))).thenReturn(new NewznabXmlRoot()); + when(customQueryAndTitleMappingHandler.mapSearchRequest(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0)); } @Test - public void shouldCache() throws Exception { + void shouldCache() throws Exception { NewznabParameters parameters = new NewznabParameters(); parameters.setQ("q"); parameters.setApikey("apikey"); @@ -116,7 +119,7 @@ public class ExternalApiTest { } @Test - public void shouldRepeatSearchWhenCacheTimeIsOver() throws Exception { + void shouldRepeatSearchWhenCacheTimeIsOver() throws Exception { NewznabParameters parameters = new NewznabParameters(); parameters.setQ("q"); parameters.setApikey("apikey"); @@ -135,7 +138,7 @@ public class ExternalApiTest { } @Test - public void shouldCacheRemoveEntriesWhenLimitReached() throws Exception { + void shouldCacheRemoveEntriesWhenLimitReached() throws Exception { NewznabParameters parameters = getNewznabParameters("q1"); testee.api(parameters, null, null); @@ -170,9 +173,9 @@ public class ExternalApiTest { } @Test - public void shouldUseCorrectHeaders() throws Exception { + void shouldUseCorrectHeaders() throws Exception { NewznabJsonRoot jsonRoot = new NewznabJsonRoot(); - when(newznabJsonTransformerMock.transformToRoot(any(), any(), anyInt(), any())).thenReturn(jsonRoot); + when(newznabJsonTransformerMock.transformToRoot(any(), any(), anyInt(), any(Boolean.class))).thenReturn(jsonRoot); NewznabParameters parameters = new NewznabParameters(); parameters.setQ("q1"); parameters.setApikey("apikey"); @@ -183,7 +186,7 @@ public class ExternalApiTest { assertThat(responseEntity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON_UTF8); NewznabXmlRoot xmlRoot = new NewznabXmlRoot(); - when(newznabXmlTransformerMock.getRssRoot(any(), any(), anyInt(), any())).thenReturn(xmlRoot); + when(newznabXmlTransformerMock.getRssRoot(any(), any(), anyInt(), any(Boolean.class))).thenReturn(xmlRoot); parameters.setO(OutputType.XML); responseEntity = testee.api(parameters, null, null); diff --git a/core/src/test/java/org/nzbhydra/api/NewznabXmlTransformerTest.java b/core/src/test/java/org/nzbhydra/api/NewznabXmlTransformerTest.java index 4158b3c4b..ef8bb3330 100644 --- a/core/src/test/java/org/nzbhydra/api/NewznabXmlTransformerTest.java +++ b/core/src/test/java/org/nzbhydra/api/NewznabXmlTransformerTest.java @@ -16,25 +16,25 @@ package org.nzbhydra.api; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.MainConfig; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.category.Category; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.downloading.FileHandler; import org.nzbhydra.indexers.Indexer; import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; import org.nzbhydra.searching.SearchResult; -import org.nzbhydra.searching.dtoseventsenums.DownloadType; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -54,7 +54,7 @@ public class NewznabXmlTransformerTest { IndexerConfig indexerConfig = new IndexerConfig(); - @Before + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); when(configProvider.getBaseConfig()).thenReturn(baseConfig); @@ -76,7 +76,7 @@ public class NewznabXmlTransformerTest { @Test - public void shouldUseCorrectApplicationType() { + void shouldUseCorrectApplicationType() { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); SearchResultItem searchResultItem = new SearchResultItem(); searchResultItem.setIndexer(indexerMock); @@ -92,4 +92,4 @@ public class NewznabXmlTransformerTest { } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/backup/BackupTaskTest.java b/core/src/test/java/org/nzbhydra/backup/BackupTaskTest.java index 0c0f41d21..70a708f1d 100644 --- a/core/src/test/java/org/nzbhydra/backup/BackupTaskTest.java +++ b/core/src/test/java/org/nzbhydra/backup/BackupTaskTest.java @@ -1,8 +1,12 @@ package org.nzbhydra.backup; -import org.junit.Before; -import org.junit.Test; -import org.mockito.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.genericstorage.GenericStorage; @@ -14,8 +18,7 @@ import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.Optional; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -34,7 +37,7 @@ public class BackupTaskTest { @InjectMocks private BackupTask testee = new BackupTask(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -47,59 +50,59 @@ public class BackupTaskTest { } @Test - public void shouldCreateBackupIfEnabledAndNoBackupExecutedYet() throws Exception { + void shouldCreateBackupIfEnabledAndNoBackupExecutedYet() throws Exception { when(genericStorage.get("FirstStart", LocalDateTime.class)).thenReturn(Optional.of(LocalDateTime.now(testee.clock).minus(200, ChronoUnit.DAYS))); testee.createBackup(); - verify(backupAndRestore).backup(); + verify(backupAndRestore).backup(false); verify(genericStorage).save(eq("BackupData"), backupDataArgumentCaptor.capture()); BackupData backupData = backupDataArgumentCaptor.getValue(); - assertThat(backupData.getLastBackup(), is(LocalDateTime.now(testee.clock))); + assertThat(backupData.getLastBackup()).isEqualTo(LocalDateTime.now(testee.clock)); } @Test - public void shouldCreateBackupIfEnabledAndLastBackupAndStartupWasNotToday() throws Exception { + void shouldCreateBackupIfEnabledAndLastBackupAndStartupWasNotToday() throws Exception { when(genericStorage.get("FirstStart", LocalDateTime.class)).thenReturn(Optional.of(LocalDateTime.now(testee.clock).minus(200, ChronoUnit.DAYS))); when(genericStorage.get(BackupTask.KEY, BackupData.class)).thenReturn(Optional.of(new BackupData(LocalDateTime.now(testee.clock).minus(8, ChronoUnit.DAYS)))); testee.createBackup(); - verify(backupAndRestore).backup(); + verify(backupAndRestore).backup(false); verify(genericStorage).save(eq("BackupData"), backupDataArgumentCaptor.capture()); BackupData backupData = backupDataArgumentCaptor.getValue(); - assertThat(backupData.getLastBackup(), is(LocalDateTime.now(testee.clock))); + assertThat(backupData.getLastBackup()).isEqualTo(LocalDateTime.now(testee.clock)); } @Test - public void shouldNotCreateBackupIfEnabledAndLastBackupWasTooRecently() throws Exception { + void shouldNotCreateBackupIfEnabledAndLastBackupWasTooRecently() throws Exception { when(genericStorage.get("FirstStart", LocalDateTime.class)).thenReturn(Optional.of(LocalDateTime.now(testee.clock).minus(200, ChronoUnit.DAYS))); when(genericStorage.get(BackupTask.KEY, BackupData.class)).thenReturn(Optional.of(new BackupData(LocalDateTime.now(testee.clock).minus(5, ChronoUnit.DAYS)))); testee.createBackup(); - verify(backupAndRestore, never()).backup(); + verify(backupAndRestore, never()).backup(false); verify(genericStorage, never()).save(eq("BackupData"), backupDataArgumentCaptor.capture()); } @Test - public void shouldNotCreateBackupIfDisabled() throws Exception { + void shouldNotCreateBackupIfDisabled() throws Exception { config.getMain().setBackupEveryXDays(null); testee.createBackup(); - verify(backupAndRestore, never()).backup(); + verify(backupAndRestore, never()).backup(false); } @Test - public void shouldNotCreateBackupIfStartedToday() throws Exception { + void shouldNotCreateBackupIfStartedToday() throws Exception { when(genericStorage.get("FirstStart", LocalDateTime.class)).thenReturn(Optional.of(LocalDateTime.now(testee.clock))); testee.createBackup(); - verify(backupAndRestore, never()).backup(); + verify(backupAndRestore, never()).backup(false); } @Test - public void shouldNotCreateBackupIfLastBackupWasToday() throws Exception { + void shouldNotCreateBackupIfLastBackupWasToday() throws Exception { when(genericStorage.get("BackupData", BackupData.class)).thenReturn(Optional.of(new BackupData(LocalDateTime.now(testee.clock)))); testee.createBackup(); - verify(backupAndRestore, never()).backup(); + verify(backupAndRestore, never()).backup(false); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/config/BaseConfigTest.java b/core/src/test/java/org/nzbhydra/config/BaseConfigTest.java index 9bbe527db..81e25d00b 100644 --- a/core/src/test/java/org/nzbhydra/config/BaseConfigTest.java +++ b/core/src/test/java/org/nzbhydra/config/BaseConfigTest.java @@ -6,30 +6,26 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.google.common.collect.Sets; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.category.Category; -import org.nzbhydra.config.indexer.IndexerConfig; -import org.reflections.Reflections; +import org.nzbhydra.config.validation.BaseConfigValidator; +import org.nzbhydra.config.validation.ConfigValidationTools; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; import java.io.BufferedReader; import java.io.InputStreamReader; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map.Entry; +import java.util.Set; import java.util.stream.Collectors; -import static junit.framework.TestCase.fail; -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; public class BaseConfigTest { @@ -38,33 +34,34 @@ public class BaseConfigTest { private static final boolean COMPARE_CONFIG_VALUES = false; private Set dontCheckTheseLists = Sets.newHashSet("categories"); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); } @InjectMocks private BaseConfig testee = new BaseConfig(); + private BaseConfigValidator baseConfigValidator = new BaseConfigValidator(); @Test - public void shouldRecognizeRestartRequired() { + void shouldRecognizeRestartRequired() { MainConfig mainConfig1 = new MainConfig(); mainConfig1.setPort(123); MainConfig mainConfig2 = new MainConfig(); mainConfig2.setPort(234); - assertTrue(mainConfig1.isRestartNeeded(mainConfig2)); + assertTrue(ConfigValidationTools.isRestartNeeded(mainConfig1, mainConfig2)); mainConfig2.setPort(123); - assertFalse(mainConfig1.isRestartNeeded(mainConfig2)); + assertThat(ConfigValidationTools.isRestartNeeded(mainConfig1, mainConfig2)).isFalse(); mainConfig1.setSsl(true); mainConfig2.setSsl(false); - assertTrue(mainConfig1.isRestartNeeded(mainConfig2)); + assertTrue(ConfigValidationTools.isRestartNeeded(mainConfig1, mainConfig2)); } @Test - public void applicationPropertiesShouldHaveTheSameKeysAsConfigClasses() throws Exception { + void applicationPropertiesShouldHaveTheSameKeysAsConfigClasses() throws Exception { ObjectMapper jsonMapper = new ObjectMapper(); jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); @@ -90,73 +87,8 @@ public class BaseConfigTest { compare(mapFromApplicationYml, mapFromBaseConfig); } - @Test - public void shouldValidateIndexers() { - IndexerConfig indexerConfigMock = mock(IndexerConfig.class); - when(indexerConfigMock.validateConfig(any(), any(), any())).thenReturn(new ValidatingConfig.ConfigValidationResult(true, false, new ArrayList(), new ArrayList())); - testee.getIndexers().add(indexerConfigMock); - testee.validateConfig(new BaseConfig(), testee, new BaseConfig()); - verify(indexerConfigMock).validateConfig(any(), any(), any()); - } - @Test - public void shouldRecognizeDuplicateIndexerNames() { - IndexerConfig indexerConfigMock = mock(IndexerConfig.class); - when(indexerConfigMock.getName()).thenReturn("name"); - IndexerConfig indexerConfigMock2 = mock(IndexerConfig.class); - when(indexerConfigMock2.getName()).thenReturn("name"); - when(indexerConfigMock.validateConfig(any(), any(), any())).thenReturn(new ValidatingConfig.ConfigValidationResult(true, false, new ArrayList(), new ArrayList())); - when(indexerConfigMock2.validateConfig(any(), any(), any())).thenReturn(new ValidatingConfig.ConfigValidationResult(true, false, new ArrayList(), new ArrayList())); - testee.getIndexers().add(indexerConfigMock); - testee.getIndexers().add(indexerConfigMock2); - ValidatingConfig.ConfigValidationResult result = testee.validateConfig(new BaseConfig(), testee, new BaseConfig()); - assertEquals(3, result.getErrorMessages().size()); - assertTrue(result.getErrorMessages().get(2).contains("Duplicate")); - } - @Test - public void shouldInitializeAllListsAsEmptyInBaseConfigYaml() throws Exception { - BaseConfig baseConfig = new ConfigReaderWriter().originalConfig(); - validateListsNotNull(baseConfig); - } - - @Test - public void shouldInitializeAllListsAsEmptyInClasses() throws Exception { - Reflections reflections = new Reflections("org.nzbhydra"); - Set> classes = reflections.getSubTypesOf(ValidatingConfig.class); - for (Class configClass : classes) { - boolean constructorFound = false; - for (Constructor constructor : configClass.getDeclaredConstructors()) { - if (constructor.getParameterCount() == 0) { - ValidatingConfig config = (ValidatingConfig) constructor.newInstance(); - validateListsNotNull(config); - constructorFound = true; - break; - } - } - assertTrue("No default constructor found for class " + configClass.getName(), constructorFound); - } - } - - protected void validateListsNotNull(ValidatingConfig config) throws IntrospectionException, IllegalAccessException, InvocationTargetException { - BeanInfo beanInfo = Introspector.getBeanInfo(config.getClass()); - for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { - if (pd.getReadMethod().getReturnType().isAssignableFrom(List.class)) { - List list = (List) pd.getReadMethod().invoke(config); - assertNotNull("Property of " + config.getClass().getName() + "#" + pd.getReadMethod().getName() + " should be initialized as empty list", list); - if (!list.isEmpty()) { - if (list.get(0).getClass().getSuperclass() == ValidatingConfig.class) { - for (Object o : list) { - validateListsNotNull((ValidatingConfig) o); - } - } - } - } else if (pd.getReadMethod().getReturnType().getSuperclass() == ValidatingConfig.class) { - ValidatingConfig subConfig = (ValidatingConfig) pd.getReadMethod().invoke(config); - validateListsNotNull(subConfig); - } - } - } private void compare(Object left, Object right) { if (left instanceof HashMap) { @@ -164,13 +96,13 @@ public class BaseConfigTest { } else if (left instanceof List) { compareLists((List) left, (List) right); } else { - assertEquals("Setting in baseConfig.yml is different than in base config", left, left); + assertEquals(left, left, "Setting in baseConfig.yml is different than in base config"); } } private void compareMaps(HashMap left, HashMap right) { for (Entry entry : left.entrySet()) { - assertTrue(entry.getValue() + " is contained in baseConfig.yml but not in base config", right.containsKey(entry.getKey())); + assertTrue(right.containsKey(entry.getKey()), entry.getValue() + " is contained in baseConfig.yml but not in base config"); if (entry.getValue() instanceof LinkedHashMap) { compareMaps((HashMap) entry.getValue(), (HashMap) right.get(entry.getKey())); } else if (entry.getValue() instanceof List) { @@ -178,7 +110,7 @@ public class BaseConfigTest { compareLists((List) entry.getValue(), (List) right.get(entry.getKey())); } } else if (COMPARE_CONFIG_VALUES) { - assertEquals("Setting " + entry.getKey() + " in baseConfig.yml is different than in base config", entry.getValue(), right.get(entry.getKey())); + assertEquals(entry.getValue(), right.get(entry.getKey()), "Setting " + entry.getKey() + " in baseConfig.yml is different than in base config"); } } Set rightKeys = right.keySet(); @@ -205,15 +137,15 @@ public class BaseConfigTest { fail("Different lists. Found in right but not left: " + newRight); } } - assertEquals("Both should contain the same amount of config entries", left.size(), right.size()); + assertEquals(left.size(), right.size(), "Both should contain the same amount of config entries"); if (COMPARE_CONFIG_VALUES) { for (int i = 0; i < left.size(); i++) { Object l = left.get(i); - assertEquals(l.getClass(), right.getClass()); + assertThat(right.getClass()).isEqualTo(l.getClass()); if (l instanceof Category) { - assertTrue("Both categories should be the same", ((Category) l).deepEquals((Category) right.get(i))); + assertTrue(((Category) l).deepEquals((Category) right.get(i)), "Both categories should be the same"); } else { - assertEquals("Setting in baseConfig.yml is different than in base config", l, right.get(i)); + assertEquals(l, right.get(i), "Setting in baseConfig.yml is different than in base config"); } } } diff --git a/core/src/test/java/org/nzbhydra/config/CategoriesConfigTest.java b/core/src/test/java/org/nzbhydra/config/CategoriesConfigTest.java index 0bebcfb61..559b6f802 100644 --- a/core/src/test/java/org/nzbhydra/config/CategoriesConfigTest.java +++ b/core/src/test/java/org/nzbhydra/config/CategoriesConfigTest.java @@ -1,24 +1,22 @@ package org.nzbhydra.config; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.nzbhydra.config.ValidatingConfig.ConfigValidationResult; +import org.junit.jupiter.api.Test; import org.nzbhydra.config.category.CategoriesConfig; import org.nzbhydra.config.category.Category; +import org.nzbhydra.config.validation.CategoriesConfigValidator; +import org.nzbhydra.config.validation.ConfigValidationResult; import java.util.Arrays; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class CategoriesConfigTest { - @InjectMocks - private CategoriesConfig testee = new CategoriesConfig(); + private final CategoriesConfig testee = new CategoriesConfig(); + private final CategoriesConfigValidator categoriesConfigValidator = new CategoriesConfigValidator(); @Test - public void shouldValidateTorrentsFolder() throws Exception { + void shouldValidateTorrentsFolder() throws Exception { BaseConfig baseConfig = new BaseConfig(); Category moviesCategory = new Category("Movies"); @@ -37,14 +35,14 @@ public class CategoriesConfigTest { } private void validateAndCheckForSublevelError(BaseConfig baseConfig) { - ConfigValidationResult result = testee.validateConfig(baseConfig, testee, null); - assertThat(result.getWarningMessages().size(), is(1)); - assertThat(result.getWarningMessages().get(0), containsString("sublevel")); + ConfigValidationResult result = categoriesConfigValidator.validateConfig(baseConfig, null, testee); + assertThat(result.getWarningMessages().size()).isEqualTo(1); + assertThat(result.getWarningMessages().get(0)).contains("sublevel"); } private void validateAndCheckForNoError(BaseConfig baseConfig) { - ConfigValidationResult result = testee.validateConfig(baseConfig, testee, null); - assertThat(result.getWarningMessages().size(), is(0)); + ConfigValidationResult result = categoriesConfigValidator.validateConfig(baseConfig, null, testee); + assertThat(result.getWarningMessages().size()).isEqualTo(0); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/config/DownloadingConfigTest.java b/core/src/test/java/org/nzbhydra/config/DownloadingConfigTest.java index 46942ab5b..6007e438e 100644 --- a/core/src/test/java/org/nzbhydra/config/DownloadingConfigTest.java +++ b/core/src/test/java/org/nzbhydra/config/DownloadingConfigTest.java @@ -1,48 +1,48 @@ package org.nzbhydra.config; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; -import org.nzbhydra.config.ValidatingConfig.ConfigValidationResult; import org.nzbhydra.config.downloading.DownloadingConfig; +import org.nzbhydra.config.validation.ConfigValidationResult; +import org.nzbhydra.config.validation.DownloadingConfigValidator; import java.io.File; import java.io.PrintWriter; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class DownloadingConfigTest { @InjectMocks private DownloadingConfig testee = new DownloadingConfig(); + private DownloadingConfigValidator downloadingConfigValidator = new DownloadingConfigValidator(); @Test - public void shouldValidateTorrentsFolder() throws Exception { + void shouldValidateTorrentsFolder() throws Exception { BaseConfig baseConfig = new BaseConfig(); testee.setSaveTorrentsTo("relative"); new File("relative").deleteOnExit(); - ConfigValidationResult result = testee.validateConfig(baseConfig, testee, new BaseConfig()); - assertThat(result.getErrorMessages().size(), is(1)); - assertThat(result.getErrorMessages().get(0), containsString("not absolute")); + ConfigValidationResult result = downloadingConfigValidator.validateConfig(baseConfig, new BaseConfig(), testee); + assertThat(result.getErrorMessages().size()).isEqualTo(1); + assertThat(result.getErrorMessages().get(0)).contains("not absolute"); File afile = new File("afile.txt"); afile.deleteOnExit(); PrintWriter out = new PrintWriter("afile.txt"); out.write("out"); testee.setSaveTorrentsTo(afile.getAbsolutePath()); - result = testee.validateConfig(baseConfig, testee, new BaseConfig()); - assertThat(result.getErrorMessages().size(), is(1)); - assertThat(result.getErrorMessages().get(0), containsString("is a file")); + result = downloadingConfigValidator.validateConfig(baseConfig, new BaseConfig(), testee); + assertThat(result.getErrorMessages().size()).isEqualTo(1); + assertThat(result.getErrorMessages().get(0)).contains("is a file"); File folder = new File(""); testee.setSaveTorrentsTo(folder.getAbsolutePath()); - result = testee.validateConfig(baseConfig, testee, new BaseConfig()); - assertThat(result.getErrorMessages().size(), is(0)); + result = downloadingConfigValidator.validateConfig(baseConfig, new BaseConfig(), testee); + assertThat(result.getErrorMessages().size()).isEqualTo(0); afile.delete(); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/config/IndexerCategoryConfigTest.java b/core/src/test/java/org/nzbhydra/config/IndexerCategoryConfigTest.java index 0070fa637..c01a226ce 100644 --- a/core/src/test/java/org/nzbhydra/config/IndexerCategoryConfigTest.java +++ b/core/src/test/java/org/nzbhydra/config/IndexerCategoryConfigTest.java @@ -1,6 +1,6 @@ package org.nzbhydra.config; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.nzbhydra.config.indexer.IndexerCategoryConfig; import org.nzbhydra.config.indexer.IndexerCategoryConfig.MainCategory; @@ -8,19 +8,19 @@ import org.nzbhydra.config.indexer.IndexerCategoryConfig.SubCategory; import java.util.Arrays; -import static junit.framework.TestCase.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class IndexerCategoryConfigTest { @InjectMocks private IndexerCategoryConfig testee = new IndexerCategoryConfig(); @Test - public void getNameFromId() throws Exception { + void getNameFromId() throws Exception { testee.setCategories(Arrays.asList(new MainCategory(1000, "1000", Arrays.asList(new SubCategory(1010, "1010"))))); - assertEquals(testee.getNameFromId(1000), "1000"); - assertEquals(testee.getNameFromId(1010), "1000 1010"); - assertEquals(testee.getNameFromId(1234), "N/A"); + assertThat("1000").isEqualTo(testee.getNameFromId(1000)); + assertThat("1000 1010").isEqualTo(testee.getNameFromId(1010)); + assertThat("N/A").isEqualTo(testee.getNameFromId(1234)); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/config/IndexerConfigTest.java b/core/src/test/java/org/nzbhydra/config/IndexerConfigTest.java index 560cd2639..51bc1985a 100644 --- a/core/src/test/java/org/nzbhydra/config/IndexerConfigTest.java +++ b/core/src/test/java/org/nzbhydra/config/IndexerConfigTest.java @@ -16,10 +16,11 @@ package org.nzbhydra.config; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; -import org.nzbhydra.config.ValidatingConfig.ConfigValidationResult; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.validation.ConfigValidationResult; +import org.nzbhydra.config.validation.IndexerConfigValidator; import java.util.Arrays; @@ -29,21 +30,22 @@ public class IndexerConfigTest { @InjectMocks private IndexerConfig testee = new IndexerConfig(); + private IndexerConfigValidator indexerConfigValidator = new IndexerConfigValidator(); @Test - public void shouldValidateSchedules() { + void shouldValidateSchedules() { testee.setSchedule(Arrays.asList("blabla")); testee.setName("indexer"); BaseConfig baseConfig = new BaseConfig(); baseConfig.setIndexers(Arrays.asList(testee)); - ConfigValidationResult result = testee.validateConfig(baseConfig, testee, null); + ConfigValidationResult result = indexerConfigValidator.validateConfig(baseConfig, null, testee); assertThat(result.getErrorMessages()).containsExactly("Indexer indexer contains an invalid schedule: blabla"); testee.setSchedule(Arrays.asList("mo8-10")); - result = testee.validateConfig(baseConfig, testee, null); + result = indexerConfigValidator.validateConfig(baseConfig, null, testee); assertThat(result.getErrorMessages()).isEmpty(); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/config/SearchSourceRestrictionTest.java b/core/src/test/java/org/nzbhydra/config/SearchSourceRestrictionTest.java index bf9ac9403..28f3e0d0d 100644 --- a/core/src/test/java/org/nzbhydra/config/SearchSourceRestrictionTest.java +++ b/core/src/test/java/org/nzbhydra/config/SearchSourceRestrictionTest.java @@ -16,72 +16,73 @@ package org.nzbhydra.config; -import org.junit.Test; -import org.nzbhydra.mediainfo.MediaIdType; -import org.nzbhydra.searching.dtoseventsenums.SearchType; +import org.junit.jupiter.api.Test; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; public class SearchSourceRestrictionTest { @Test - public void shouldMeetCorrectlyForApiSearches() { + void shouldMeetCorrectlyForApiSearches() { Map identifiers = new HashMap<>(); identifiers.put(MediaIdType.IMDB, "imdbId"); - SearchRequest apiSearchRequest = new SearchRequest(SearchRequest.SearchSource.API, SearchType.SEARCH, 0, 0); + SearchRequest apiSearchRequest = new SearchRequest(SearchSource.API, SearchType.SEARCH, 0, 0); apiSearchRequest.setQuery("query"); - assertThat(SearchSourceRestriction.NONE.meets(apiSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.BOTH.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.API.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.INTERNAL.meets(apiSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.ALL_BUT_RSS.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.ONLY_RSS.meets(apiSearchRequest)).isFalse(); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.NONE)).isFalse(); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.BOTH)); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.API)); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.INTERNAL)).isFalse(); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.ALL_BUT_RSS)); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.ONLY_RSS)).isFalse(); apiSearchRequest.setQuery(null); - assertThat(SearchSourceRestriction.NONE.meets(apiSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.BOTH.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.API.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.INTERNAL.meets(apiSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.ALL_BUT_RSS.meets(apiSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.ONLY_RSS.meets(apiSearchRequest)).isTrue(); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.NONE)).isFalse(); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.BOTH)); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.API)); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.INTERNAL)).isFalse(); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.ALL_BUT_RSS)).isFalse(); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.ONLY_RSS)); apiSearchRequest.setIdentifiers(identifiers); - assertThat(SearchSourceRestriction.NONE.meets(apiSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.BOTH.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.API.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.INTERNAL.meets(apiSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.ALL_BUT_RSS.meets(apiSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.ONLY_RSS.meets(apiSearchRequest)).isFalse(); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.NONE)).isFalse(); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.BOTH)); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.API)); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.INTERNAL)).isFalse(); + assertTrue(apiSearchRequest.meets(SearchSourceRestriction.ALL_BUT_RSS)); + assertThat(apiSearchRequest.meets(SearchSourceRestriction.ONLY_RSS)).isFalse(); } @Test - public void shouldMeetCorrectlyForInternalSearches() { + void shouldMeetCorrectlyForInternalSearches() { Map identifiers = new HashMap<>(); identifiers.put(MediaIdType.IMDB, "imdbId"); - SearchRequest internalSearchRequest = new SearchRequest(SearchRequest.SearchSource.INTERNAL, SearchType.SEARCH, 0, 0); + SearchRequest internalSearchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 0); internalSearchRequest.setQuery("query"); - assertThat(SearchSourceRestriction.NONE.meets(internalSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.BOTH.meets(internalSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.API.meets(internalSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.INTERNAL.meets(internalSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.ALL_BUT_RSS.meets(internalSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.ONLY_RSS.meets(internalSearchRequest)).isFalse(); + assertThat(internalSearchRequest.meets(SearchSourceRestriction.NONE)).isFalse(); + assertTrue(internalSearchRequest.meets(SearchSourceRestriction.BOTH)); + assertThat(internalSearchRequest.meets(SearchSourceRestriction.API)).isFalse(); + assertTrue(internalSearchRequest.meets(SearchSourceRestriction.INTERNAL)); + assertTrue(internalSearchRequest.meets(SearchSourceRestriction.ALL_BUT_RSS)); + assertThat(internalSearchRequest.meets(SearchSourceRestriction.ONLY_RSS)).isFalse(); //No RSS / update / empty searches for internal internalSearchRequest.setIdentifiers(identifiers); - assertThat(SearchSourceRestriction.NONE.meets(internalSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.BOTH.meets(internalSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.API.meets(internalSearchRequest)).isFalse(); - assertThat(SearchSourceRestriction.INTERNAL.meets(internalSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.ALL_BUT_RSS.meets(internalSearchRequest)).isTrue(); - assertThat(SearchSourceRestriction.ONLY_RSS.meets(internalSearchRequest)).isFalse(); + assertThat(internalSearchRequest.meets(SearchSourceRestriction.NONE)).isFalse(); + assertTrue(internalSearchRequest.meets(SearchSourceRestriction.BOTH)); + assertThat(internalSearchRequest.meets(SearchSourceRestriction.API)).isFalse(); + assertTrue(internalSearchRequest.meets(SearchSourceRestriction.INTERNAL)); + assertTrue(internalSearchRequest.meets(SearchSourceRestriction.ALL_BUT_RSS)); + assertThat(internalSearchRequest.meets(SearchSourceRestriction.ONLY_RSS)).isFalse(); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationStep005to006Test.java b/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationStep005to006Test.java index a665deb18..d8901c46c 100644 --- a/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationStep005to006Test.java +++ b/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationStep005to006Test.java @@ -17,7 +17,7 @@ package org.nzbhydra.config.migration; import com.google.common.io.Resources; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.nzbhydra.Jackson; import org.nzbhydra.config.BaseConfig; @@ -35,7 +35,7 @@ public class ConfigMigrationStep005to006Test { @Test - public void migrate() throws Exception { + void migrate() throws Exception { String yaml = Resources.toString(ConfigMigrationStep005to006Test.class.getResource("migrate5to6.yaml"), Charset.defaultCharset()); Map map = Jackson.YAML_MAPPER.readValue(yaml, ConfigReaderWriter.MAP_TYPE_REFERENCE); diff --git a/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationTest.java b/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationTest.java index 545cf44d1..88a5f3f87 100644 --- a/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationTest.java +++ b/core/src/test/java/org/nzbhydra/config/migration/ConfigMigrationTest.java @@ -19,8 +19,8 @@ package org.nzbhydra.config.migration; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -34,6 +34,8 @@ import java.util.List; import java.util.Map; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -51,14 +53,14 @@ public class ConfigMigrationTest { }; private ObjectMapper objectMapper = new ObjectMapper(); - @Before + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); objectMapper.registerModule(new Jdk8Module()); } @Test - public void shouldMigrate() { + void shouldMigrate() { BaseConfig input = new BaseConfig(); input.getMain().setConfigVersion(1); BaseConfig afterMigration = new BaseConfig(); @@ -77,26 +79,28 @@ public class ConfigMigrationTest { assertThat(input.getMain().getConfigVersion()).isEqualTo(2); } - @Test(expected = RuntimeException.class) - public void shouldThrowExceptionIfWrongConfigVersionAfterMigration() { - BaseConfig input = new BaseConfig(); - input.getMain().setConfigVersion(1); - Map map = objectMapper.convertValue(input, typeRef); + @Test + void shouldThrowExceptionIfWrongConfigVersionAfterMigration() { + assertThrows(RuntimeException.class, () -> { + BaseConfig input = new BaseConfig(); + input.getMain().setConfigVersion(1); + Map map = objectMapper.convertValue(input, typeRef); - testee.steps = Collections.emptyList(); //Just don't call any steps, this will skip the loop, not increasing the version - testee.expectedConfigVersion = 2; + testee.steps = Collections.emptyList(); //Just don't call any steps, this will skip the loop, not increasing the version + testee.expectedConfigVersion = 2; - testee.migrate(map); + testee.migrate(map); + }); } @Test - public void shouldFindMigrationStepsForAllPossibleConfigVersions() { + void shouldFindMigrationStepsForAllPossibleConfigVersions() { Integer currentConfigVersion = new MainConfig().getConfigVersion(); List steps = ConfigMigration.getMigrationSteps(); for (int i = 3; i < currentConfigVersion; i++) { int finalI = i; - assertThat(steps.stream().anyMatch(x -> x.forVersion() == finalI)).isTrue(); + assertTrue(steps.stream().anyMatch(x -> x.forVersion() == finalI)); } } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/database/ThreadedTest.java b/core/src/test/java/org/nzbhydra/database/ThreadedTest.java index 30c5e1bed..a8c24aeb5 100644 --- a/core/src/test/java/org/nzbhydra/database/ThreadedTest.java +++ b/core/src/test/java/org/nzbhydra/database/ThreadedTest.java @@ -1,33 +1,34 @@ package org.nzbhydra.database; import com.google.common.base.Stopwatch; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.nzbhydra.NzbHydra; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.searching.db.SearchEntity; import org.nzbhydra.searching.db.SearchRepository; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @SpringBootTest(classes = NzbHydra.class) -@DataJpaTest -@Ignore +@AutoConfigureDataJpa +//@DataJpaTest +@Disabled public class ThreadedTest { @Autowired SearchRepository searchRepository; @Test - public void executeParallelSaves() throws Exception { + void executeParallelSaves() throws Exception { ExecutorService pool = Executors.newFixedThreadPool(50); for (int i = 0; i < 50; i++) { pool.execute(() -> { @@ -48,4 +49,4 @@ public class ThreadedTest { } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/downloading/DownloadResultTest.java b/core/src/test/java/org/nzbhydra/downloading/DownloadResultTest.java index 9c18eec8a..fd2e25d1e 100644 --- a/core/src/test/java/org/nzbhydra/downloading/DownloadResultTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/DownloadResultTest.java @@ -16,9 +16,9 @@ package org.nzbhydra.downloading; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.searching.db.SearchResultEntity; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import org.springframework.http.HttpHeaders; import static org.assertj.core.api.Assertions.assertThat; @@ -26,25 +26,25 @@ import static org.assertj.core.api.Assertions.assertThat; public class DownloadResultTest { @Test - public void shouldBuildFilenameCorrectly() { + void shouldBuildFilenameCorrectly() { SearchResultEntity searchResultEntity = new SearchResultEntity(); - searchResultEntity.setDownloadType(SearchResultItem.DownloadType.NZB); + searchResultEntity.setDownloadType(DownloadType.NZB); FileDownloadEntity nzbDownloadEntity = new FileDownloadEntity(); nzbDownloadEntity.setSearchResult(searchResultEntity); DownloadResult testee = DownloadResult.createSuccessfulDownloadResult("title", "content".getBytes(), nzbDownloadEntity); assertThat(testee.getAsResponseEntity().getHeaders().get(HttpHeaders.CONTENT_DISPOSITION)).containsExactly("attachment; filename=\"title.nzb\""); - searchResultEntity.setDownloadType(SearchResultItem.DownloadType.TORRENT); + searchResultEntity.setDownloadType(DownloadType.TORRENT); testee = DownloadResult.createSuccessfulDownloadResult("title", "content".getBytes(), nzbDownloadEntity); assertThat(testee.getAsResponseEntity().getHeaders().get(HttpHeaders.CONTENT_DISPOSITION)).containsExactly("attachment; filename=\"title.torrent\""); } @Test - public void shouldCleanMagnetLink() { + void shouldCleanMagnetLink() { FileDownloadEntity nzbDownloadEntity = new FileDownloadEntity(); - DownloadResult testee = DownloadResult.createSuccessfulRedirectResult("title","magnet:?xt=urn:btih:738c4612aefe678bf76aa8e2e4fbacf8bd541&dn=Some Guy S06E35.Title.WEB.h264-GROUP" , nzbDownloadEntity); + DownloadResult testee = DownloadResult.createSuccessfulRedirectResult("title", "magnet:?xt=urn:btih:738c4612aefe678bf76aa8e2e4fbacf8bd541&dn=Some Guy S06E35.Title.WEB.h264-GROUP", nzbDownloadEntity); String cleanedUrl = testee.getCleanedUrl(); assertThat(cleanedUrl).contains("Some+Guy+S06E35.Title.WEB.h264-GROUP"); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/downloading/DownloadStatusUpdaterTest.java b/core/src/test/java/org/nzbhydra/downloading/DownloadStatusUpdaterTest.java index a053cc5dc..2375a0a64 100644 --- a/core/src/test/java/org/nzbhydra/downloading/DownloadStatusUpdaterTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/DownloadStatusUpdaterTest.java @@ -16,8 +16,8 @@ package org.nzbhydra.downloading; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -51,7 +52,7 @@ public class DownloadStatusUpdaterTest { @InjectMocks private DownloadStatusUpdater testee; - @Before + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); when(downloaderProvider.getAllDownloaders()).thenReturn(Collections.singletonList(downloaderMock)); @@ -60,7 +61,7 @@ public class DownloadStatusUpdaterTest { } @Test - public void shouldNotRunWhenNotEnabled() { + void shouldNotRunWhenNotEnabled() { testee.queueCheckEnabled = false; testee.lastDownload = Instant.now(); testee.checkStatus(Collections.singletonList(FileDownloadStatus.REQUESTED), 10000, StatusCheckType.QUEUE); @@ -68,7 +69,7 @@ public class DownloadStatusUpdaterTest { } @Test - public void shouldNotRunWhenLastDownloadTooLongGone() { + void shouldNotRunWhenLastDownloadTooLongGone() { testee.queueCheckEnabled = true; testee.lastDownload = Instant.ofEpochSecond(1L); testee.checkStatus(Collections.singletonList(FileDownloadStatus.REQUESTED), 10000, StatusCheckType.HISTORY); @@ -76,7 +77,7 @@ public class DownloadStatusUpdaterTest { } @Test - public void shouldSetDisabledAndNotRunIfNoDownloadsInDatabase() { + void shouldSetDisabledAndNotRunIfNoDownloadsInDatabase() { testee.historyCheckEnabled = true; testee.lastDownload = Instant.now(); List statuses = Collections.singletonList(FileDownloadStatus.REQUESTED); @@ -87,14 +88,14 @@ public class DownloadStatusUpdaterTest { } @Test - public void shouldCallDownloader() { + void shouldCallDownloader() { testee.historyCheckEnabled = true; testee.lastDownload = Instant.now(); List statuses = Collections.singletonList(FileDownloadStatus.REQUESTED); List downloadsWaitingForUpdate = Collections.singletonList(new FileDownloadEntity()); when(downloadRepository.findByStatusInAndTimeAfterOrderByTimeDesc(eq(statuses), any())).thenReturn(downloadsWaitingForUpdate); List downloadsReturnedFromDownloader = Collections.singletonList(new FileDownloadEntity()); - when(downloaderMock.checkForStatusUpdates(any(),eq(StatusCheckType.HISTORY))).thenReturn(downloadsReturnedFromDownloader); + when(downloaderMock.checkForStatusUpdates(any(), eq(StatusCheckType.HISTORY))).thenReturn(downloadsReturnedFromDownloader); testee.checkStatus(statuses, 10000, StatusCheckType.HISTORY); @@ -103,14 +104,14 @@ public class DownloadStatusUpdaterTest { } @Test - public void shouldSetEnabledOnDownloadEvent() { + void shouldSetEnabledOnDownloadEvent() { testee.queueCheckEnabled = false; testee.lastDownload = null; testee.onNzbDownloadEvent(new FileDownloadEvent(new FileDownloadEntity(), new SearchResultEntity())); - assertThat(testee.queueCheckEnabled).isTrue(); + assertTrue(testee.queueCheckEnabled); assertThat(testee.lastDownload).isNotNull(); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/downloading/FileDownloadEntityTest.java b/core/src/test/java/org/nzbhydra/downloading/FileDownloadEntityTest.java new file mode 100644 index 000000000..c65f6bfd6 --- /dev/null +++ b/core/src/test/java/org/nzbhydra/downloading/FileDownloadEntityTest.java @@ -0,0 +1,69 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.downloading; + +import com.fasterxml.jackson.databind.ObjectWriter; +import org.junit.jupiter.api.Test; +import org.nzbhydra.Jackson; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.downloading.DownloadType; +import org.nzbhydra.config.downloading.FileDownloadAccessType; +import org.nzbhydra.indexers.IndexerEntity; +import org.nzbhydra.searching.db.SearchEntity; +import org.nzbhydra.searching.db.SearchResultEntity; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +class FileDownloadEntityTest { + + @Test + public void shouldBeConvertibleToTest() throws Exception { + FileDownloadEntity testee = new FileDownloadEntity(); + testee.setTime(Instant.now()); + testee.setIp("ip"); + testee.setAge(1); + testee.setUsername("username"); + testee.setStatus(FileDownloadStatus.CONTENT_DOWNLOAD_SUCCESSFUL); + testee.setExternalId("externalId"); + testee.setAccessSource(SearchSource.INTERNAL); + testee.setNzbAccessType(FileDownloadAccessType.PROXY); + final SearchResultEntity searchResult = new SearchResultEntity(); + searchResult.setIndexerGuid("indexerGuid"); + searchResult.setLink("link"); + searchResult.setTitle("title"); + searchResult.setDetails("details"); + searchResult.setFirstFound(Instant.now()); + searchResult.setPubDate(Instant.now()); + searchResult.setDownloadType(DownloadType.NZB); + final IndexerEntity indexerEntity = new IndexerEntity("indexerName"); + searchResult.setIndexer(indexerEntity); + final SearchEntity searchEntity = new SearchEntity(); + searchEntity.setQuery("query"); + searchResult.setIndexerSearchEntityId(1); + + testee.setSearchResult(searchResult); + + final ObjectWriter printer = Jackson.JSON_MAPPER.writerWithDefaultPrettyPrinter(); + final FileDownloadEntityTO to = Jackson.JSON_MAPPER.convertValue(testee, FileDownloadEntityTO.class); + final String jsonTO = printer.writeValueAsString(to); + final String jsonEntity = printer.writeValueAsString(testee); + assertThat(jsonTO).isEqualTo(jsonEntity); + } + +} diff --git a/core/src/test/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaverTest.java b/core/src/test/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaverTest.java index 830699e88..74f2bdf95 100644 --- a/core/src/test/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaverTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/IndexerUniquenessScoreSaverTest.java @@ -17,9 +17,10 @@ package org.nzbhydra.downloading; import com.google.common.collect.Sets; +import jakarta.persistence.EntityManagerFactory; import org.hibernate.Session; import org.hibernate.SessionFactory; -import org.junit.Before; +import org.junit.jupiter.api.BeforeEach; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Captor; @@ -28,6 +29,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.downloading.FileDownloadAccessType; import org.nzbhydra.indexers.IndexerEntity; import org.nzbhydra.indexers.IndexerSearchEntity; @@ -35,17 +37,17 @@ import org.nzbhydra.indexers.IndexerSearchRepository; import org.nzbhydra.searching.db.SearchEntity; import org.nzbhydra.searching.db.SearchResultEntity; import org.nzbhydra.searching.db.SearchResultRepository; -import org.nzbhydra.searching.searchrequests.SearchRequest; import org.nzbhydra.searching.uniqueness.IndexerUniquenessScoreEntity; import org.nzbhydra.searching.uniqueness.IndexerUniquenessScoreEntityRepository; -import javax.persistence.EntityManagerFactory; import java.time.Instant; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; @@ -76,7 +78,7 @@ public class IndexerUniquenessScoreSaverTest { @InjectMocks private IndexerUniquenessScoreSaver testee = new IndexerUniquenessScoreSaver(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); final BaseConfig value = new BaseConfig(); @@ -104,25 +106,28 @@ public class IndexerUniquenessScoreSaverTest { indexerHasNot.setName("indexerHasNot"); indexerHasNot.setId(3); - IndexerSearchEntity indexerSearchEntityHasDownloaded = new IndexerSearchEntity(indexerHasDownloaded, searchEntity); + IndexerSearchEntity indexerSearchEntityHasDownloaded = new IndexerSearchEntity(indexerHasDownloaded, searchEntity, new Random().nextInt()); indexerSearchEntityHasDownloaded.setSuccessful(true); - IndexerSearchEntity indexerSearchEntityhasToo = new IndexerSearchEntity(indexerhasToo, searchEntity); + IndexerSearchEntity indexerSearchEntityhasToo = new IndexerSearchEntity(indexerhasToo, searchEntity, new Random().nextInt()); indexerSearchEntityhasToo.setSuccessful(true); - IndexerSearchEntity indexerSearchEntityHasNot = new IndexerSearchEntity(indexerHasNot, searchEntity); + IndexerSearchEntity indexerSearchEntityHasNot = new IndexerSearchEntity(indexerHasNot, searchEntity, new Random().nextInt()); indexerSearchEntityHasNot.setSuccessful(true); SearchResultEntity searchResultEntityHasDownloaded = new SearchResultEntity(indexerHasDownloaded, Instant.now(), "Some.result-with_different.Characters", "", "", "", null, Instant.now()); - searchResultEntityHasDownloaded.setIndexerSearchEntity(indexerSearchEntityHasDownloaded); + searchResultEntityHasDownloaded.setIndexerSearchEntityId(indexerSearchEntityHasDownloaded.getId()); SearchResultEntity searchResultEntityhasToo = new SearchResultEntity(indexerhasToo, Instant.now(), "", "", "", "", null, null); - searchResultEntityhasToo.setIndexerSearchEntity(indexerSearchEntityhasToo); + searchResultEntityhasToo.setIndexerSearchEntityId(indexerSearchEntityhasToo.getId()); SearchResultEntity searchResultEntityhasNot = new SearchResultEntity(indexerHasNot, Instant.now(), "", "", "", "", null, null); - searchResultEntityhasNot.setIndexerSearchEntity(indexerSearchEntityHasNot); + searchResultEntityhasNot.setIndexerSearchEntityId(indexerSearchEntityHasNot.getId()); - FileDownloadEntity fileDownloadEntity = new FileDownloadEntity(searchResultEntityHasDownloaded, FileDownloadAccessType.REDIRECT, SearchRequest.SearchSource.API, FileDownloadStatus.NONE, null); + FileDownloadEntity fileDownloadEntity = new FileDownloadEntity(searchResultEntityHasDownloaded, FileDownloadAccessType.REDIRECT, SearchSource.API, FileDownloadStatus.NONE, null); FileDownloadEvent downloadEvent = new FileDownloadEvent(fileDownloadEntity, searchResultEntityHasDownloaded); when(searchResultRepository.findAllByTitleLikeIgnoreCase(anyString())).thenReturn(Sets.newHashSet(searchResultEntityHasDownloaded, searchResultEntityhasToo)); when(indexerSearchRepository.findBySearchEntity(searchEntity)).thenReturn(Sets.newHashSet(indexerSearchEntityHasDownloaded, indexerSearchEntityhasToo, indexerSearchEntityHasNot)); + when(indexerSearchRepository.getReferenceById(indexerSearchEntityHasDownloaded.getId())).thenReturn(indexerSearchEntityHasDownloaded); + when(indexerSearchRepository.getReferenceById(indexerSearchEntityhasToo.getId())).thenReturn(indexerSearchEntityhasToo); + when(indexerSearchRepository.getReferenceById(indexerSearchEntityHasNot.getId())).thenReturn(indexerSearchEntityHasNot); testee.onNzbDownloadEvent(downloadEvent); when(sessionMock.load(ArgumentMatchers.eq(SearchResultEntity.class), any())).thenReturn(new SearchResultEntity()); @@ -134,13 +139,13 @@ public class IndexerUniquenessScoreSaverTest { assertThat(score1.getIndexer()).isEqualTo(indexerHasDownloaded); assertThat(score1.getInvolved()).isEqualTo(3); assertThat(score1.getHave()).isEqualTo(2); - assertThat(score1.isHasResult()).isTrue(); + assertTrue(score1.isHasResult()); IndexerUniquenessScoreEntity score2 = scoreCaptor.getValue().get(1); assertThat(score2.getIndexer()).isEqualTo(indexerhasToo); assertThat(score2.getInvolved()).isEqualTo(3); assertThat(score2.getHave()).isEqualTo(2); - assertThat(score2.isHasResult()).isTrue(); + assertTrue(score2.isHasResult()); IndexerUniquenessScoreEntity score3 = scoreCaptor.getValue().get(2); assertThat(score3.getIndexer()).isEqualTo(indexerHasNot); @@ -163,24 +168,28 @@ public class IndexerUniquenessScoreSaverTest { IndexerEntity indexerHasNot2 = new IndexerEntity("indexerHasNot2"); indexerHasNot2.setId(3); - IndexerSearchEntity indexerSearchEntityHasDownloaded = new IndexerSearchEntity(indexerHasDownloaded, searchEntity); + IndexerSearchEntity indexerSearchEntityHasDownloaded = new IndexerSearchEntity(indexerHasDownloaded, searchEntity, new Random().nextInt()); indexerSearchEntityHasDownloaded.setSuccessful(true); indexerSearchEntityHasDownloaded.setId(1); - IndexerSearchEntity indexerSearchEntityhasNot2 = new IndexerSearchEntity(indexerHasNot2, searchEntity); + IndexerSearchEntity indexerSearchEntityhasNot2 = new IndexerSearchEntity(indexerHasNot2, searchEntity, new Random().nextInt()); indexerSearchEntityhasNot2.setSuccessful(true); indexerSearchEntityhasNot2.setId(2); - IndexerSearchEntity indexerSearchEntityHasNot = new IndexerSearchEntity(indexerHasNot, searchEntity); + IndexerSearchEntity indexerSearchEntityHasNot = new IndexerSearchEntity(indexerHasNot, searchEntity, new Random().nextInt()); indexerSearchEntityHasNot.setSuccessful(true); indexerSearchEntityHasNot.setId(3); + when(indexerSearchRepository.getReferenceById(indexerSearchEntityHasDownloaded.getId())).thenReturn(indexerSearchEntityHasDownloaded); + when(indexerSearchRepository.getReferenceById(indexerSearchEntityHasNot.getId())).thenReturn(indexerSearchEntityHasNot); + when(indexerSearchRepository.getReferenceById(indexerSearchEntityhasNot2.getId())).thenReturn(indexerSearchEntityhasNot2); SearchResultEntity searchResultEntityHasDownloaded = new SearchResultEntity(indexerHasDownloaded, Instant.now(), "", "", "", "", null, Instant.now()); - searchResultEntityHasDownloaded.setIndexerSearchEntity(indexerSearchEntityHasDownloaded); + searchResultEntityHasDownloaded.setIndexerSearchEntityId(indexerSearchEntityHasDownloaded.getId()); SearchResultEntity searchResultEntityhasNot2 = new SearchResultEntity(indexerHasNot2, Instant.now(), "", "", "", "", null, null); - searchResultEntityhasNot2.setIndexerSearchEntity(indexerSearchEntityhasNot2); + searchResultEntityhasNot2.setIndexerSearchEntityId(indexerSearchEntityhasNot2.getId()); SearchResultEntity searchResultEntityhasNot = new SearchResultEntity(indexerHasNot, Instant.now(), "", "", "", "", null, null); - searchResultEntityhasNot.setIndexerSearchEntity(indexerSearchEntityHasNot); + searchResultEntityhasNot.setIndexerSearchEntityId(indexerSearchEntityHasNot.getId()); - FileDownloadEntity fileDownloadEntity = new FileDownloadEntity(searchResultEntityHasDownloaded, FileDownloadAccessType.REDIRECT, SearchRequest.SearchSource.API, FileDownloadStatus.NONE, null); + + FileDownloadEntity fileDownloadEntity = new FileDownloadEntity(searchResultEntityHasDownloaded, FileDownloadAccessType.REDIRECT, SearchSource.API, FileDownloadStatus.NONE, null); FileDownloadEvent downloadEvent = new FileDownloadEvent(fileDownloadEntity, searchResultEntityHasDownloaded); when(searchResultRepository.findAllByTitleLikeIgnoreCase(anyString())).thenReturn(Sets.newHashSet(searchResultEntityHasDownloaded)); @@ -198,7 +207,7 @@ public class IndexerUniquenessScoreSaverTest { assertThat(score1.getIndexer()).isEqualTo(indexerHasDownloaded); assertThat(score1.getInvolved()).isEqualTo(3); assertThat(score1.getHave()).isEqualTo(1); - assertThat(score1.isHasResult()).isTrue(); + assertTrue(score1.isHasResult()); IndexerUniquenessScoreEntity score2 = scores.get(1); assertThat(score2.getIndexer()).isEqualTo(indexerHasNot); diff --git a/core/src/test/java/org/nzbhydra/downloading/NzbDownloadEntityTest.java b/core/src/test/java/org/nzbhydra/downloading/NzbDownloadEntityTest.java index 6cd055966..ec492c262 100644 --- a/core/src/test/java/org/nzbhydra/downloading/NzbDownloadEntityTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/NzbDownloadEntityTest.java @@ -16,7 +16,7 @@ package org.nzbhydra.downloading; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -24,10 +24,10 @@ public class NzbDownloadEntityTest { @Test - public void shouldTruncatLongError() { + void shouldTruncatLongError() { FileDownloadEntity testee = new FileDownloadEntity(); StringBuilder builder = new StringBuilder(); - for (int i = 1; i<=799;i++) { + for (int i = 1; i <= 799; i++) { builder.append("12345"); } builder.append("abcdef"); diff --git a/core/src/test/java/org/nzbhydra/downloading/NzbDownloadStatusTest.java b/core/src/test/java/org/nzbhydra/downloading/NzbDownloadStatusTest.java index 82404ec83..5364678c6 100644 --- a/core/src/test/java/org/nzbhydra/downloading/NzbDownloadStatusTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/NzbDownloadStatusTest.java @@ -1,25 +1,25 @@ -package org.nzbhydra.downloading; - -import org.junit.Test; - -import static org.junit.Assert.assertTrue; - -public class NzbDownloadStatusTest { - - @Test - public void testCanUpdate() { - assertTrue(FileDownloadStatus.NZB_ADDED.canUpdate(FileDownloadStatus.NONE)); - assertTrue(FileDownloadStatus.NZB_ADDED.canUpdate(FileDownloadStatus.REQUESTED)); - assertTrue(FileDownloadStatus.NZB_DOWNLOAD_SUCCESSFUL.canUpdate(FileDownloadStatus.REQUESTED)); - assertTrue(FileDownloadStatus.NZB_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NONE)); - assertTrue(FileDownloadStatus.NZB_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.REQUESTED)); - assertTrue(FileDownloadStatus.NZB_ADD_REJECTED.canUpdate(FileDownloadStatus.REQUESTED)); - assertTrue(FileDownloadStatus.NZB_ADD_REJECTED.canUpdate(FileDownloadStatus.NZB_DOWNLOAD_SUCCESSFUL)); - assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NONE)); - assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NZB_DOWNLOAD_SUCCESSFUL)); - assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NZB_ADDED)); - assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.REQUESTED)); - } - - -} \ No newline at end of file +package org.nzbhydra.downloading; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NzbDownloadStatusTest { + + @Test + void testCanUpdate() { + assertTrue(FileDownloadStatus.NZB_ADDED.canUpdate(FileDownloadStatus.NONE)); + assertTrue(FileDownloadStatus.NZB_ADDED.canUpdate(FileDownloadStatus.REQUESTED)); + assertTrue(FileDownloadStatus.NZB_DOWNLOAD_SUCCESSFUL.canUpdate(FileDownloadStatus.REQUESTED)); + assertTrue(FileDownloadStatus.NZB_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NONE)); + assertTrue(FileDownloadStatus.NZB_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.REQUESTED)); + assertTrue(FileDownloadStatus.NZB_ADD_REJECTED.canUpdate(FileDownloadStatus.REQUESTED)); + assertTrue(FileDownloadStatus.NZB_ADD_REJECTED.canUpdate(FileDownloadStatus.NZB_DOWNLOAD_SUCCESSFUL)); + assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NONE)); + assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NZB_DOWNLOAD_SUCCESSFUL)); + assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.NZB_ADDED)); + assertTrue(FileDownloadStatus.CONTENT_DOWNLOAD_ERROR.canUpdate(FileDownloadStatus.REQUESTED)); + } + + +} diff --git a/core/src/test/java/org/nzbhydra/downloading/NzbGetTest.java b/core/src/test/java/org/nzbhydra/downloading/NzbGetTest.java deleted file mode 100644 index 0278eb3c4..000000000 --- a/core/src/test/java/org/nzbhydra/downloading/NzbGetTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.nzbhydra.downloading; - -import org.junit.Test; -import org.mockito.InjectMocks; -import org.nzbhydra.downloading.downloaders.nzbget.NzbGet; -import org.springframework.web.util.UriComponentsBuilder; - -import java.net.URI; - -public class NzbGetTest { - @InjectMocks - private NzbGet testee = new NzbGet(); - - @Test - public void intialize() throws Throwable { - String originalUrl = "http://nzbget:nzbget?@127.0.0.1:6789/jsonrpc"; - System.out.println(originalUrl); - -// URL url = new URL(originalUrl); -// System.out.println(url); - - URI builtUri = UriComponentsBuilder.fromHttpUrl(originalUrl).build().toUri(); - System.out.println(builtUri.toString()); - - // Map headers = new HashMap<>(); -// headers.put("Authorization", "Basic " + BaseEncoding.base64().encode("nzbget:nzbget?:".getBytes())); -// new JsonRpcHttpClient(url, headers).invoke("writelog", new Object[]{"INFO", "NZBHydra connected to test connection"}, String.class); - -// DownloaderConfig config = new DownloaderConfig(); -// config.setUrl("http://127.0.0.1:6789"); -// config.setUsername("nzbget"); -// config.setPassword(""); -// testee.intialize(config); -// testee.getHistory(Instant.now()); - } - - -} \ No newline at end of file diff --git a/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderStatusTest.java b/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderStatusTest.java index 83f6da711..565465040 100644 --- a/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderStatusTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderStatusTest.java @@ -16,7 +16,7 @@ package org.nzbhydra.downloading.downloaders; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import java.util.ArrayList; @@ -32,7 +32,7 @@ public class DownloaderStatusTest { @Test - public void getDownloadingRatesInKilobytes() { + void getDownloadingRatesInKilobytes() { List list = new ArrayList<>(Arrays.asList(100L, 100L, 100L, 100L, 100L, 100L, 100L)); testee.setDownloadingRatesInKilobytes(list); assertThat(testee.getDownloadingRatesInKilobytes()).containsExactlyElementsOf(list); diff --git a/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderTest.java b/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderTest.java index 4eff4be47..936424e49 100644 --- a/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/downloaders/DownloaderTest.java @@ -16,9 +16,9 @@ package org.nzbhydra.downloading.downloaders; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -38,7 +38,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -@Ignore +@Disabled public class DownloaderTest { @Mock @@ -54,7 +54,7 @@ public class DownloaderTest { private Downloader testee = Mockito.mock(Downloader.class, Mockito.CALLS_REAL_METHODS); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); testee.nzbHandler = nzbHandler; @@ -66,14 +66,14 @@ public class DownloaderTest { } @Test - public void shouldReturnWhenGivenNoDownloads() throws Exception{ + void shouldReturnWhenGivenNoDownloads() throws Exception { testee.checkForStatusUpdates(Collections.emptyList(), StatusCheckType.HISTORY); verify(testee, never()).getHistory(any()); } @Test - public void shouldSkipDownloadsWithoutSearchResult() throws Exception{ + void shouldSkipDownloadsWithoutSearchResult() throws Exception { when(testee.getHistory(any())).thenReturn(Collections.singletonList(downloaderEntry)); testee.checkForStatusUpdates(Collections.singletonList(new FileDownloadEntity()), StatusCheckType.HISTORY); @@ -82,7 +82,7 @@ public class DownloaderTest { } @Test - public void shouldSkipDownloadEntriesWithUnparsableStatus() throws Exception{ + void shouldSkipDownloadEntriesWithUnparsableStatus() throws Exception { when(testee.getHistory(any())).thenReturn(Collections.singletonList(downloaderEntry)); when(testee.getDownloadStatusFromDownloaderEntry(any(), any())).thenReturn(null); when(testee.isDownloadMatchingDownloaderEntry(any(), any())).thenReturn(true); @@ -93,7 +93,7 @@ public class DownloaderTest { } @Test - public void shouldSetNewStatusIfUpdates() throws Exception{ + void shouldSetNewStatusIfUpdates() throws Exception { when(testee.getHistory(any())).thenReturn(Collections.singletonList(downloaderEntry)); when(testee.getDownloadStatusFromDownloaderEntry(any(), any())).thenReturn(FileDownloadStatus.NZB_DOWNLOAD_SUCCESSFUL); when(testee.isDownloadMatchingDownloaderEntry(any(), any())).thenReturn(true); @@ -104,7 +104,7 @@ public class DownloaderTest { } @Test - public void shouldSkipNewStatusIfNotUpdates() throws Exception{ + void shouldSkipNewStatusIfNotUpdates() throws Exception { when(testee.getHistory(any())).thenReturn(Collections.singletonList(downloaderEntry)); when(testee.getDownloadStatusFromDownloaderEntry(any(), any())).thenReturn(FileDownloadStatus.NONE); when(testee.isDownloadMatchingDownloaderEntry(any(), any())).thenReturn(true); @@ -115,7 +115,7 @@ public class DownloaderTest { } @Test - public void shouldSetExternalIdIfNotSetBefore() throws Exception{ + void shouldSetExternalIdIfNotSetBefore() throws Exception { when(downloaderEntry.getNzbId()).thenReturn("nzbId"); when(testee.getDownloadStatusFromDownloaderEntry(any(), any())).thenReturn(FileDownloadStatus.NZB_ADDED); when(testee.getQueue(any())).thenReturn(Collections.singletonList(downloaderEntry)); diff --git a/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/HistoryResponseTest.java b/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/HistoryResponseTest.java index 67147c5bc..d1fd677ee 100644 --- a/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/HistoryResponseTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/HistoryResponseTest.java @@ -19,7 +19,7 @@ package org.nzbhydra.downloading.downloaders.sabnzbd; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.nzbhydra.downloading.downloaders.sabnzbd.mapping.HistoryResponse; import java.io.IOException; @@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class HistoryResponseTest { @Test - public void shouldParseHistoryResponseWithoutErrors() throws IOException { + void shouldParseHistoryResponseWithoutErrors() throws IOException { String json = Resources.toString(Resources.getResource(HistoryResponseTest.class, "historyResponse.json"), Charsets.UTF_8); ObjectMapper objectMapper = new ObjectMapper(); HistoryResponse response = objectMapper.readValue(json, HistoryResponse.class); diff --git a/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/QueueResponseTest.java b/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/QueueResponseTest.java index c67e553c3..789e9be4c 100644 --- a/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/QueueResponseTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/downloaders/sabnzbd/QueueResponseTest.java @@ -19,7 +19,7 @@ package org.nzbhydra.downloading.downloaders.sabnzbd; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.nzbhydra.downloading.downloaders.sabnzbd.mapping.QueueResponse; import java.io.IOException; @@ -27,7 +27,7 @@ import java.io.IOException; public class QueueResponseTest { @Test - public void shouldParseQueueResponseWithoutErrors() throws IOException { + void shouldParseQueueResponseWithoutErrors() throws IOException { String json = Resources.toString(Resources.getResource(QueueResponseTest.class, "queueResponse.json"), Charsets.UTF_8); ObjectMapper objectMapper = new ObjectMapper(); QueueResponse response = objectMapper.readValue(json, QueueResponse.class); diff --git a/core/src/test/java/org/nzbhydra/downloading/torrents/TorrentFileHandlerTest.java b/core/src/test/java/org/nzbhydra/downloading/torrents/TorrentFileHandlerTest.java index 007f93034..962c46110 100644 --- a/core/src/test/java/org/nzbhydra/downloading/torrents/TorrentFileHandlerTest.java +++ b/core/src/test/java/org/nzbhydra/downloading/torrents/TorrentFileHandlerTest.java @@ -16,9 +16,10 @@ package org.nzbhydra.downloading.torrents; +import org.apache.commons.lang3.SystemUtils; import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -39,32 +40,39 @@ public class TorrentFileHandlerTest { @InjectMocks private TorrentFileHandler testee; - @Before + String saveTorrentsTo; + + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); final BaseConfig baseConfig = new BaseConfig(); when(configProviderMock.getBaseConfig()).thenReturn(baseConfig); - baseConfig.getDownloading().setSaveTorrentsTo("c:\\torrents"); + if (SystemUtils.IS_OS_WINDOWS) { + saveTorrentsTo = "c:\\torrents"; + } else { + saveTorrentsTo = "/torrents"; + } + baseConfig.getDownloading().setSaveTorrentsTo(saveTorrentsTo); } @Test - public void shouldCalculateTorrentFilePath() { + void shouldCalculateTorrentFilePath() { final File targetFile = testee.getTargetFile(DownloadResult.createSuccessfulDownloadResult("Some title", "".getBytes(), null), null); - Assertions.assertThat(targetFile).isEqualTo(new File("c:\\torrents\\Some title.torrent")); + Assertions.assertThat(targetFile).isEqualTo(new File(saveTorrentsTo, "Some title.torrent")); } @Test - public void shouldShortenFileName() { - final File targetFile = testee.getTargetFile(DownloadResult.createSuccessfulDownloadResult("Some title that is so long that the resulting path exceeds 220 characters which is roughly the max length of a path on windows and perhaps even some linux systems, not sure about that. I think 255 is the limit but let's just be sure, no filename should be that long anyway.", "".getBytes(), null), null); - Assertions.assertThat(targetFile).isEqualTo(new File("c:\\torrents\\Some title that is so long that the resulting path exceeds 220 characters which is roughly the max length of a path on windows and perhaps even some linux systems, not sure about that. I think 255 is .torrent")); - Assertions.assertThat(targetFile.getAbsolutePath()).hasSize(220); + void shouldShortenFileName() { + final String title = "Some title that is so long that the resulting path exceeds 220 characters which is roughly the max length of a path on windows and perhaps even some linux systems, not sure about that. I think 255 is the limit but let's just be sure, no filename should be that long anyway."; + final File targetFile = testee.getTargetFile(DownloadResult.createSuccessfulDownloadResult(title, "".getBytes(), null), null); + Assertions.assertThat(targetFile.getAbsolutePath()).hasSizeLessThan(230); } @Test - public void shouldCalculateMagnetFilePath() throws Exception { + void shouldCalculateMagnetFilePath() throws Exception { final File targetFile = testee.getTargetFile(DownloadResult.createSuccessfulDownloadResult("Some title", "".getBytes(), null), new URI("http://127.0.0.1")); - Assertions.assertThat(targetFile).isEqualTo(new File("c:\\torrents\\Some title.magnet")); + Assertions.assertThat(targetFile).isEqualTo(new File(saveTorrentsTo, "Some title.magnet")); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/externaltools/ExternalToolsTest.java b/core/src/test/java/org/nzbhydra/externaltools/ExternalToolsTest.java index c6ed94118..373a8637f 100644 --- a/core/src/test/java/org/nzbhydra/externaltools/ExternalToolsTest.java +++ b/core/src/test/java/org/nzbhydra/externaltools/ExternalToolsTest.java @@ -16,7 +16,7 @@ package org.nzbhydra.externaltools; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.nzbhydra.Jackson; import java.util.Comparator; @@ -24,17 +24,17 @@ import java.util.Comparator; public class ExternalToolsTest { @Test - public void bla() throws Exception { + void bla() throws Exception { String json = "{\n" + - " \"enableRss\" : true,\n" + - " \"enableAutomaticSearch\" : true,\n" + - " \"enableInteractiveSearch\" : true,\n" + - " \"supportsRss\" : true,\n" + - " \"supportsSearch\" : true,\n" + - " \"protocol\" : \"torrent\",\n" + - " \"name\" : \"NZBHydra2 (mocktorz1)\",\n" + - " \"fields\" : [ {\n" + - " \"name\" : \"apiKey\",\n" + + " \"enableRss\" : true,\n" + + " \"enableAutomaticSearch\" : true,\n" + + " \"enableInteractiveSearch\" : true,\n" + + " \"supportsRss\" : true,\n" + + " \"supportsSearch\" : true,\n" + + " \"protocol\" : \"torrent\",\n" + + " \"name\" : \"NZBHydra2 (mocktorz1)\",\n" + + " \"fields\" : [ {\n" + + " \"name\" : \"apiKey\",\n" + " \"value\" : \"apikey\"\n" + " }, {\n" + " \"name\" : \"categories\",\n" + @@ -77,4 +77,4 @@ public class ExternalToolsTest { } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/fortests/NewznabResponseBuilderTest.java b/core/src/test/java/org/nzbhydra/fortests/NewznabResponseBuilderTest.java index 8eb821cde..e79fc5c76 100644 --- a/core/src/test/java/org/nzbhydra/fortests/NewznabResponseBuilderTest.java +++ b/core/src/test/java/org/nzbhydra/fortests/NewznabResponseBuilderTest.java @@ -1,9 +1,9 @@ package org.nzbhydra.fortests; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class NewznabResponseBuilderTest { @@ -11,26 +11,26 @@ public class NewznabResponseBuilderTest { @Test - public void shouldBuild() { + void shouldBuild() { NewznabXmlRoot root = testee.getTestResult(1, 2, "itemTitle", null, null); - assertEquals(2, root.getRssChannel().getItems().size()); - assertEquals("itemTitle1", root.getRssChannel().getItems().get(0).getTitle()); + assertThat(root.getRssChannel().getItems()).hasSize(2); + assertThat(root.getRssChannel().getItems().get(0).getTitle()).isEqualTo("itemTitle1"); root = testee.getTestResult(3, 3, "itemTitle", null, null); - assertEquals(1, root.getRssChannel().getItems().size()); - assertEquals("itemTitle3", root.getRssChannel().getItems().get(0).getTitle()); + assertThat(root.getRssChannel().getItems()).hasSize(1); + assertThat(root.getRssChannel().getItems().get(0).getTitle()).isEqualTo("itemTitle3"); } @Test - public void shouldInsertOffsetAndTotal() { + void shouldInsertOffsetAndTotal() { NewznabXmlRoot root = testee.getTestResult(1, 2, "itemTitle", null, null); - assertEquals(0, root.getRssChannel().getNewznabResponse().getOffset().intValue()); - assertEquals(2, root.getRssChannel().getNewznabResponse().getTotal().intValue()); + assertThat(root.getRssChannel().getNewznabResponse().getOffset().intValue()).isEqualTo(0); + assertThat(root.getRssChannel().getNewznabResponse().getTotal().intValue()).isEqualTo(2); root = testee.getTestResult(1, 2, "itemTitle", 2, 100); - assertEquals(2, root.getRssChannel().getNewznabResponse().getOffset().intValue()); - assertEquals(100, root.getRssChannel().getNewznabResponse().getTotal().intValue()); + assertThat(root.getRssChannel().getNewznabResponse().getOffset().intValue()).isEqualTo(2); + assertThat(root.getRssChannel().getNewznabResponse().getTotal().intValue()).isEqualTo(100); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/historystats/StatsComponentTest.java b/core/src/test/java/org/nzbhydra/historystats/StatsComponentTest.java index faa67b3ed..5308942d4 100644 --- a/core/src/test/java/org/nzbhydra/historystats/StatsComponentTest.java +++ b/core/src/test/java/org/nzbhydra/historystats/StatsComponentTest.java @@ -1,307 +1,320 @@ -package org.nzbhydra.historystats; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.nzbhydra.NzbHydra; -import org.nzbhydra.config.indexer.IndexerConfig; -import org.nzbhydra.config.indexer.SearchModuleType; -import org.nzbhydra.downloading.FileDownloadEntity; -import org.nzbhydra.downloading.FileDownloadRepository; -import org.nzbhydra.historystats.stats.*; -import org.nzbhydra.indexers.*; -import org.nzbhydra.searching.SearchModuleConfigProvider; -import org.nzbhydra.searching.SearchModuleProvider; -import org.nzbhydra.searching.db.SearchEntity; -import org.nzbhydra.searching.db.SearchRepository; -import org.nzbhydra.searching.db.SearchResultEntity; -import org.nzbhydra.searching.db.SearchResultRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.List; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - -@SuppressWarnings("SpringJavaAutowiringInspection") -@RunWith(SpringRunner.class) -@SpringBootTest(classes = NzbHydra.class) -@Ignore // Doesn't run since upgrade to Spring Boot 2.1 -public class StatsComponentTest { - - private IndexerEntity indexer1; - private IndexerEntity indexer2; - private IndexerConfig indexerConfig1; - private IndexerConfig indexerConfig2; - - @Autowired - private SearchModuleConfigProvider searchModuleConfigProvider; - @Autowired - private SearchModuleProvider searchModuleProvider; - @Autowired - private IndexerApiAccessRepository apiAccessRepository; - @Autowired - private IndexerRepository indexerRepository; - @Autowired - private SearchRepository searchRepository; - @Autowired - private FileDownloadRepository downloadRepository; - @Autowired - private SearchResultRepository searchResultRepository; - @Autowired - private IndexerSearchRepository indexerSearchRepository; - - @Autowired - private Stats stats; - - @Before - public void setUp() { - indexerRepository.deleteAll(); - apiAccessRepository.deleteAll(); - indexerConfig1 = new IndexerConfig(); - indexerConfig1.setName("indexer1"); - indexerConfig1.setSearchModuleType(SearchModuleType.NEWZNAB); - indexerConfig1.setState(IndexerConfig.State.ENABLED); - indexerConfig1.setHost("somehost"); - indexerConfig2 = new IndexerConfig(); - indexerConfig2.setName("indexer2"); - indexerConfig2.setSearchModuleType(SearchModuleType.NEWZNAB); - indexerConfig2.setState(IndexerConfig.State.DISABLED_USER); - indexerConfig2.setHost("somehost"); - searchModuleConfigProvider.setIndexers(Arrays.asList(indexerConfig1, indexerConfig2)); - searchModuleProvider.loadIndexers(Arrays.asList(indexerConfig1, indexerConfig2)); - indexer1 = indexerRepository.findByName("indexer1"); - indexer2 = indexerRepository.findByName("indexer2"); - } - - @Test - public void shouldCalculateAverageResponseTimes() throws Exception { - assertEquals(2, indexerRepository.count()); - - IndexerApiAccessEntity apiAccess1 = new IndexerApiAccessEntity(indexer1); - apiAccess1.setResponseTime(1000L); - apiAccess1.setTime(Instant.now().minus(1, ChronoUnit.DAYS)); - apiAccessRepository.save(apiAccess1); - - IndexerApiAccessEntity apiAccess2 = new IndexerApiAccessEntity(indexer1); - apiAccess2.setResponseTime(2000L); - apiAccess2.setTime(Instant.now().minus(1, ChronoUnit.DAYS)); - apiAccessRepository.save(apiAccess2); - - IndexerApiAccessEntity apiAccess3 = new IndexerApiAccessEntity(indexer1); - apiAccess3.setResponseTime(4000L); - apiAccess3.setTime(Instant.now().minus(100, ChronoUnit.DAYS)); - apiAccessRepository.save(apiAccess3); - - //Access #3 is not included - List averageResponseTimes = stats.averageResponseTimes(new StatsRequest(Instant.now().minus(10, ChronoUnit.DAYS), Instant.now(), true)); - assertEquals(1, averageResponseTimes.size()); - assertEquals(1500D, averageResponseTimes.get(0).getAvgResponseTime(), 0D); - - //Access #3 is included - averageResponseTimes = stats.averageResponseTimes(new StatsRequest(Instant.now().minus(101, ChronoUnit.DAYS), Instant.now(), true)); - assertEquals(1, averageResponseTimes.size()); - assertEquals(2333D, averageResponseTimes.get(0).getAvgResponseTime(), 0D); - } - - @Test - public void shouldCalculateSearchesPerDayOfWeek() throws Exception { - SearchEntity searchFriday = new SearchEntity(); - searchFriday.setTime(Instant.ofEpochSecond(1490945310L)); //Friday - SearchEntity searchThursday1 = new SearchEntity(); - searchThursday1.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday - SearchEntity searchThursday2 = new SearchEntity(); - searchThursday2.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday - SearchEntity searchSunday = new SearchEntity(); - searchSunday.setTime(Instant.ofEpochSecond(1490513310L)); //Sunday - searchRepository.saveAll(Arrays.asList(searchFriday, searchThursday1, searchThursday2, searchSunday)); - - - StatsRequest statsRequest = new StatsRequest(searchFriday.getTime().minus(10, ChronoUnit.DAYS), searchFriday.getTime().plus(10, ChronoUnit.DAYS), true); - List result = stats.countPerDayOfWeek("SEARCH", statsRequest); - assertEquals(7, result.size()); - - assertEquals("Thu", result.get(3).getDay()); - assertEquals(Integer.valueOf(2), result.get(3).getCount()); - - assertEquals("Fri", result.get(4).getDay()); - assertEquals(Integer.valueOf(1), result.get(4).getCount()); - - assertEquals("Sun", result.get(6).getDay()); - assertEquals(Integer.valueOf(1), result.get(6).getCount()); - } - - @Test - public void shouldCalculateDownloadsPerDayOfWeek() throws Exception { - - FileDownloadEntity downloadFriday = new FileDownloadEntity(); - downloadFriday.setTime(Instant.ofEpochSecond(1490945310L)); //Friday - - FileDownloadEntity downloadThursday1 = new FileDownloadEntity(); - downloadThursday1.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday - - FileDownloadEntity downloadThursday2 = new FileDownloadEntity(); - downloadThursday2.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday - - - FileDownloadEntity downloadSunday = new FileDownloadEntity(); - downloadSunday.setTime(Instant.ofEpochSecond(1490513310L)); //Sunday - - downloadRepository.saveAll(Arrays.asList(downloadFriday, downloadSunday, downloadThursday1, downloadThursday2)); - - - List result = stats.countPerDayOfWeek("INDEXERNZBDOWNLOAD", new StatsRequest(downloadFriday.getTime().minus(10, ChronoUnit.DAYS), downloadFriday.getTime().plus(10, ChronoUnit.DAYS), true)); - assertEquals(7, result.size()); - - assertEquals("Thu", result.get(3).getDay()); - assertEquals(Integer.valueOf(2), result.get(3).getCount()); - - assertEquals("Fri", result.get(4).getDay()); - assertEquals(Integer.valueOf(1), result.get(4).getCount()); - - assertEquals("Sun", result.get(6).getDay()); - assertEquals(Integer.valueOf(1), result.get(6).getCount()); - } - - @Test - public void shouldCalculateDownloadsAges() throws Exception { - - FileDownloadEntity download1 = new FileDownloadEntity(); - download1.setAge(10); - FileDownloadEntity download2 = new FileDownloadEntity(); - download2.setAge(1000); - FileDownloadEntity download3 = new FileDownloadEntity(); - download3.setAge(1500); - FileDownloadEntity download4 = new FileDownloadEntity(); - download4.setAge(2001); - FileDownloadEntity download5 = new FileDownloadEntity(); - download5.setAge(3499); - FileDownloadEntity download6 = new FileDownloadEntity(); - download6.setAge(3400); - - downloadRepository.saveAll(Arrays.asList(download1, download2, download3, download4, download5, download6)); - - List downloadPerAges = stats.downloadsPerAge(); - assertThat(downloadPerAges.get(34).getAge(), is(3400)); - assertThat(downloadPerAges.get(34).getCount(), is(2)); - - DownloadPerAgeStats downloadPerAgeStats = stats.downloadsPerAgeStats(); - assertThat(downloadPerAgeStats.getAverageAge(), is(1901)); - assertThat(downloadPerAgeStats.getPercentOlder1000(), is(66)); - assertThat(downloadPerAgeStats.getPercentOlder2000(), is(50)); - assertThat(downloadPerAgeStats.getPercentOlder3000(), is(33)); - } - - @Test - public void shouldCalculateIndexerDownloadShares() throws Exception { - FileDownloadEntity download1 = new FileDownloadEntity(); - SearchResultEntity searchResultEntity1 = getSearchResultEntity(indexer1, "1"); - download1.setSearchResult(searchResultEntity1); - - FileDownloadEntity download2 = new FileDownloadEntity(); - SearchResultEntity searchResultEntity2 = getSearchResultEntity(indexer1, "2"); - download2.setSearchResult(searchResultEntity2); - - FileDownloadEntity download3 = new FileDownloadEntity(); - SearchResultEntity searchResultEntity3 = getSearchResultEntity(indexer1, "3"); - download3.setSearchResult(searchResultEntity3); - - FileDownloadEntity download4 = new FileDownloadEntity(); - SearchResultEntity searchResultEntity4 = getSearchResultEntity(indexer1, "4"); - download4.setSearchResult(searchResultEntity4); - - FileDownloadEntity download5 = new FileDownloadEntity(); - SearchResultEntity searchResultEntity5 = getSearchResultEntity(indexer2, "5"); - download5.setSearchResult(searchResultEntity5); - - FileDownloadEntity download6 = new FileDownloadEntity(); - SearchResultEntity searchResultEntity6 = getSearchResultEntity(indexer2, "6"); - download6.setSearchResult(searchResultEntity6); - - searchResultRepository.saveAll(Arrays.asList(searchResultEntity1, searchResultEntity2, searchResultEntity3, searchResultEntity4, searchResultEntity5, searchResultEntity6)); - downloadRepository.saveAll(Arrays.asList(download1, download2, download3, download4, download5, download6)); - - List shares = stats.indexerDownloadShares(new StatsRequest(Instant.now().minus(100, ChronoUnit.DAYS), Instant.now().plus(1, ChronoUnit.DAYS), true)); - assertThat(shares.get(0).getIndexerName(), is("indexer1")); - assertThat((int) shares.get(0).getShare(), is(66)); - assertThat((int) shares.get(1).getShare(), is(33)); - } - - protected SearchResultEntity getSearchResultEntity(IndexerEntity indexer1, String title) { - SearchResultEntity searchResultEntity1 = new SearchResultEntity(); - searchResultEntity1.setIndexer(indexer1); - searchResultEntity1.setTitle(title); - searchResultEntity1.setIndexerGuid(title); - return searchResultEntity1; - } - - @Test - @Ignore //Doesn't run on CircleCI for some reason - public void shouldCalculateSearchesPerHourOfDay() throws Exception { - SearchEntity search12 = new SearchEntity(); - search12.setTime(Instant.ofEpochSecond(1490955803L)); - SearchEntity search16a = new SearchEntity(); - search16a.setTime(Instant.ofEpochSecond(1490971572L)); - SearchEntity search16b = new SearchEntity(); - search16b.setTime(Instant.ofEpochSecond(1490971572L)); - SearchEntity search23 = new SearchEntity(); - search23.setTime(Instant.ofEpochSecond(1490996779L)); - searchRepository.saveAll(Arrays.asList(search12, search16a, search16b, search23)); - - List result = stats.countPerHourOfDay("SEARCH", new StatsRequest(search12.getTime().minus(10, ChronoUnit.DAYS), search12.getTime().plus(10, ChronoUnit.DAYS), true)); - assertEquals(24, result.size()); - assertEquals(Integer.valueOf(1), result.get(12).getCount()); - assertEquals(Integer.valueOf(2), result.get(16).getCount()); - assertEquals(Integer.valueOf(1), result.get(23).getCount()); - } - - @Test - public void shouldCalculateIndexerApiAccessStats() throws Exception { - IndexerApiAccessEntity apiAccess1 = new IndexerApiAccessEntity(indexer1); - apiAccess1.setResult(IndexerAccessResult.CONNECTION_ERROR); //Counted as failed - apiAccess1.setTime(Instant.now().minus(24, ChronoUnit.HOURS)); - apiAccessRepository.save(apiAccess1); - - IndexerApiAccessEntity apiAccess2 = new IndexerApiAccessEntity(indexer1); - apiAccess2.setResult(IndexerAccessResult.HYDRA_ERROR); //Neither counted as successful nor as failed - apiAccessRepository.save(apiAccess2); - - IndexerApiAccessEntity apiAccess3 = new IndexerApiAccessEntity(indexer1); - apiAccess3.setResult(IndexerAccessResult.SUCCESSFUL); //Counted as successful - apiAccessRepository.save(apiAccess3); - - //Initially ignored by set time period - IndexerApiAccessEntity apiAccess4 = new IndexerApiAccessEntity(indexer1); - apiAccess4.setResult(IndexerAccessResult.SUCCESSFUL); //Counted as successful - apiAccess4.setTime(Instant.now().minus(14, ChronoUnit.DAYS)); - apiAccessRepository.save(apiAccess4); - - List result = stats.indexerApiAccesses(new StatsRequest(Instant.now().minus(10, ChronoUnit.DAYS), Instant.now().plus(10, ChronoUnit.DAYS), false)); - assertEquals(1, result.size()); - //One yesterday, two today: 1.5 on average - assertEquals(1.5D, result.get(0).getAverageAccessesPerDay(), 0D); - //One with connection error, one with another error, one successful - assertEquals(33D, result.get(0).getPercentSuccessful(), 1D); - - result = stats.indexerApiAccesses(new StatsRequest(Instant.now().minus(20, ChronoUnit.DAYS), Instant.now().plus(10, ChronoUnit.DAYS), false)); - assertEquals(1, result.size()); - //One yesterday, two today, one 14 days ago: 4/3=1.33 on average - assertEquals(1.33D, result.get(0).getAverageAccessesPerDay(), 0.01D); - //One with connection error, one with another error, two successful - assertEquals(50D, result.get(0).getPercentSuccessful(), 0D); - - //Now include diabled - result = stats.indexerApiAccesses(new StatsRequest(Instant.now().minus(10, ChronoUnit.DAYS), Instant.now().plus(10, ChronoUnit.DAYS), true)); - assertEquals(2, result.size()); - } - - - -} +package org.nzbhydra.historystats; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.nzbhydra.NzbHydra; +import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.indexer.SearchModuleType; +import org.nzbhydra.downloading.FileDownloadEntity; +import org.nzbhydra.downloading.FileDownloadRepository; +import org.nzbhydra.historystats.stats.AverageResponseTime; +import org.nzbhydra.historystats.stats.CountPerDayOfWeek; +import org.nzbhydra.historystats.stats.CountPerHourOfDay; +import org.nzbhydra.historystats.stats.DownloadPerAge; +import org.nzbhydra.historystats.stats.DownloadPerAgeStats; +import org.nzbhydra.historystats.stats.IndexerApiAccessStatsEntry; +import org.nzbhydra.historystats.stats.IndexerDownloadShare; +import org.nzbhydra.historystats.stats.StatsRequest; +import org.nzbhydra.indexers.IndexerAccessResult; +import org.nzbhydra.indexers.IndexerApiAccessEntity; +import org.nzbhydra.indexers.IndexerApiAccessRepository; +import org.nzbhydra.indexers.IndexerEntity; +import org.nzbhydra.indexers.IndexerRepository; +import org.nzbhydra.indexers.IndexerSearchRepository; +import org.nzbhydra.searching.SearchModuleConfigProvider; +import org.nzbhydra.searching.SearchModuleProvider; +import org.nzbhydra.searching.db.SearchEntity; +import org.nzbhydra.searching.db.SearchRepository; +import org.nzbhydra.searching.db.SearchResultEntity; +import org.nzbhydra.searching.db.SearchResultRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +@SuppressWarnings("SpringJavaAutowiringInspection") +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = NzbHydra.class) +public class StatsComponentTest { + + private IndexerEntity indexer1; + private IndexerEntity indexer2; + private IndexerConfig indexerConfig1; + private IndexerConfig indexerConfig2; + + @Autowired + private SearchModuleConfigProvider searchModuleConfigProvider; + @Autowired + private SearchModuleProvider searchModuleProvider; + @Autowired + private IndexerApiAccessRepository apiAccessRepository; + @Autowired + private IndexerRepository indexerRepository; + @Autowired + private SearchRepository searchRepository; + @Autowired + private FileDownloadRepository downloadRepository; + @Autowired + private SearchResultRepository searchResultRepository; + @Autowired + private IndexerSearchRepository indexerSearchRepository; + + @Autowired + private Stats stats; + + @BeforeEach + public void setUp() { +// indexerRepository.deleteAll(); +// apiAccessRepository.deleteAll(); + indexerConfig1 = new IndexerConfig(); + indexerConfig1.setName("indexer1"); + indexerConfig1.setSearchModuleType(SearchModuleType.NEWZNAB); + indexerConfig1.setState(IndexerConfig.State.ENABLED); + indexerConfig1.setHost("somehost"); + indexerConfig2 = new IndexerConfig(); + indexerConfig2.setName("indexer2"); + indexerConfig2.setSearchModuleType(SearchModuleType.NEWZNAB); + indexerConfig2.setState(IndexerConfig.State.DISABLED_USER); + indexerConfig2.setHost("somehost"); + searchModuleConfigProvider.setIndexers(Arrays.asList(indexerConfig1, indexerConfig2)); + searchModuleProvider.loadIndexers(Arrays.asList(indexerConfig1, indexerConfig2)); + indexer1 = indexerRepository.findByName("indexer1"); + indexer2 = indexerRepository.findByName("indexer2"); + } + + @Test + void shouldCalculateAverageResponseTimes() throws Exception { + assertThat(indexerRepository.count()).isEqualTo(2); + + IndexerApiAccessEntity apiAccess1 = new IndexerApiAccessEntity(indexer1); + apiAccess1.setResponseTime(1000L); + apiAccess1.setTime(Instant.now().minus(1, ChronoUnit.DAYS)); + apiAccessRepository.save(apiAccess1); + + IndexerApiAccessEntity apiAccess2 = new IndexerApiAccessEntity(indexer1); + apiAccess2.setResponseTime(2000L); + apiAccess2.setTime(Instant.now().minus(1, ChronoUnit.DAYS)); + apiAccessRepository.save(apiAccess2); + + IndexerApiAccessEntity apiAccess3 = new IndexerApiAccessEntity(indexer1); + apiAccess3.setResponseTime(4000L); + apiAccess3.setTime(Instant.now().minus(100, ChronoUnit.DAYS)); + apiAccessRepository.save(apiAccess3); + + //Access #3 is not included + List averageResponseTimes = stats.averageResponseTimes(new StatsRequest(Instant.now().minus(10, ChronoUnit.DAYS), Instant.now(), true)); + assertThat(averageResponseTimes).hasSize(1); + assertThat(averageResponseTimes.get(0).getAvgResponseTime()).isCloseTo(1500D, within(0D)); + + //Access #3 is included + averageResponseTimes = stats.averageResponseTimes(new StatsRequest(Instant.now().minus(101, ChronoUnit.DAYS), Instant.now(), true)); + assertThat(averageResponseTimes).hasSize(1); + assertThat(averageResponseTimes.get(0).getAvgResponseTime()).isCloseTo(2333D, within(0D)); + } + + @Test + void shouldCalculateSearchesPerDayOfWeek() throws Exception { + SearchEntity searchFriday = new SearchEntity(); + searchFriday.setTime(Instant.ofEpochSecond(1490945310L)); //Friday + SearchEntity searchThursday1 = new SearchEntity(); + searchThursday1.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday + SearchEntity searchThursday2 = new SearchEntity(); + searchThursday2.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday + SearchEntity searchSunday = new SearchEntity(); + searchSunday.setTime(Instant.ofEpochSecond(1490513310L)); //Sunday + searchRepository.saveAll(Arrays.asList(searchFriday, searchThursday1, searchThursday2, searchSunday)); + + + StatsRequest statsRequest = new StatsRequest(searchFriday.getTime().minus(10, ChronoUnit.DAYS), searchFriday.getTime().plus(10, ChronoUnit.DAYS), true); + List result = stats.countPerDayOfWeek("SEARCH", statsRequest); + assertThat(result).hasSize(7); + + assertThat(result.get(3).getDay()).isEqualTo("Thu"); + assertThat(result.get(3).getCount()).isEqualTo(Integer.valueOf(2)); + + assertThat(result.get(4).getDay()).isEqualTo("Fri"); + assertThat(result.get(4).getCount()).isEqualTo(Integer.valueOf(1)); + + assertThat(result.get(6).getDay()).isEqualTo("Sun"); + assertThat(result.get(6).getCount()).isEqualTo(Integer.valueOf(1)); + } + + @Test + void shouldCalculateDownloadsPerDayOfWeek() throws Exception { + + FileDownloadEntity downloadFriday = new FileDownloadEntity(); + downloadFriday.setTime(Instant.ofEpochSecond(1490945310L)); //Friday + + FileDownloadEntity downloadThursday1 = new FileDownloadEntity(); + downloadThursday1.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday + + FileDownloadEntity downloadThursday2 = new FileDownloadEntity(); + downloadThursday2.setTime(Instant.ofEpochSecond(1490858910L)); //Thursday + + + FileDownloadEntity downloadSunday = new FileDownloadEntity(); + downloadSunday.setTime(Instant.ofEpochSecond(1490513310L)); //Sunday + + downloadRepository.saveAll(Arrays.asList(downloadFriday, downloadSunday, downloadThursday1, downloadThursday2)); + + + List result = stats.countPerDayOfWeek("INDEXERNZBDOWNLOAD", new StatsRequest(downloadFriday.getTime().minus(10, ChronoUnit.DAYS), downloadFriday.getTime().plus(10, ChronoUnit.DAYS), true)); + assertThat(result).hasSize(7); + + assertThat(result.get(3).getDay()).isEqualTo("Thu"); + assertThat(result.get(3).getCount()).isEqualTo(Integer.valueOf(2)); + + assertThat(result.get(4).getDay()).isEqualTo("Fri"); + assertThat(result.get(4).getCount()).isEqualTo(Integer.valueOf(1)); + + assertThat(result.get(6).getDay()).isEqualTo("Sun"); + assertThat(result.get(6).getCount()).isEqualTo(Integer.valueOf(1)); + } + + @Test + void shouldCalculateDownloadsAges() throws Exception { + + FileDownloadEntity download1 = new FileDownloadEntity(); + download1.setAge(10); + FileDownloadEntity download2 = new FileDownloadEntity(); + download2.setAge(1000); + FileDownloadEntity download3 = new FileDownloadEntity(); + download3.setAge(1500); + FileDownloadEntity download4 = new FileDownloadEntity(); + download4.setAge(2001); + FileDownloadEntity download5 = new FileDownloadEntity(); + download5.setAge(3499); + FileDownloadEntity download6 = new FileDownloadEntity(); + download6.setAge(3400); + + downloadRepository.saveAll(Arrays.asList(download1, download2, download3, download4, download5, download6)); + + List downloadPerAges = stats.downloadsPerAge(); + assertThat(downloadPerAges.get(34).getAge()).isEqualTo(3400); + assertThat(downloadPerAges.get(34).getCount()).isEqualTo(2); + + DownloadPerAgeStats downloadPerAgeStats = stats.downloadsPerAgeStats(); + assertThat(downloadPerAgeStats.getAverageAge()).isEqualTo(1901); + assertThat(downloadPerAgeStats.getPercentOlder1000()).isEqualTo(66); + assertThat(downloadPerAgeStats.getPercentOlder2000()).isEqualTo(50); + assertThat(downloadPerAgeStats.getPercentOlder3000()).isEqualTo(33); + } + + @Test + void shouldCalculateIndexerDownloadShares() throws Exception { + FileDownloadEntity download1 = new FileDownloadEntity(); + SearchResultEntity searchResultEntity1 = getSearchResultEntity(indexer1, "1"); + download1.setSearchResult(searchResultEntity1); + + FileDownloadEntity download2 = new FileDownloadEntity(); + SearchResultEntity searchResultEntity2 = getSearchResultEntity(indexer1, "2"); + download2.setSearchResult(searchResultEntity2); + + FileDownloadEntity download3 = new FileDownloadEntity(); + SearchResultEntity searchResultEntity3 = getSearchResultEntity(indexer1, "3"); + download3.setSearchResult(searchResultEntity3); + + FileDownloadEntity download4 = new FileDownloadEntity(); + SearchResultEntity searchResultEntity4 = getSearchResultEntity(indexer1, "4"); + download4.setSearchResult(searchResultEntity4); + + FileDownloadEntity download5 = new FileDownloadEntity(); + SearchResultEntity searchResultEntity5 = getSearchResultEntity(indexer2, "5"); + download5.setSearchResult(searchResultEntity5); + + FileDownloadEntity download6 = new FileDownloadEntity(); + SearchResultEntity searchResultEntity6 = getSearchResultEntity(indexer2, "6"); + download6.setSearchResult(searchResultEntity6); + + searchResultRepository.saveAll(Arrays.asList(searchResultEntity1, searchResultEntity2, searchResultEntity3, searchResultEntity4, searchResultEntity5, searchResultEntity6)); + downloadRepository.saveAll(Arrays.asList(download1, download2, download3, download4, download5, download6)); + + List shares = stats.indexerDownloadShares(new StatsRequest(Instant.now().minus(100, ChronoUnit.DAYS), Instant.now().plus(1, ChronoUnit.DAYS), true)); + assertThat(shares.get(0).getIndexerName()).isEqualTo("indexer1"); + assertThat((int) shares.get(0).getShare()).isEqualTo(66); + assertThat((int) shares.get(1).getShare()).isEqualTo(33); + } + + protected SearchResultEntity getSearchResultEntity(IndexerEntity indexer1, String title) { + SearchResultEntity searchResultEntity1 = new SearchResultEntity(); + searchResultEntity1.setIndexer(indexer1); + searchResultEntity1.setTitle(title); + searchResultEntity1.setIndexerGuid(title); + return searchResultEntity1; + } + + //Doesn't run on CircleCI for some reason + @Test + @Disabled + void shouldCalculateSearchesPerHourOfDay() throws Exception { + SearchEntity search12 = new SearchEntity(); + search12.setTime(Instant.ofEpochSecond(1490955803L)); + SearchEntity search16a = new SearchEntity(); + search16a.setTime(Instant.ofEpochSecond(1490971572L)); + SearchEntity search16b = new SearchEntity(); + search16b.setTime(Instant.ofEpochSecond(1490971572L)); + SearchEntity search23 = new SearchEntity(); + search23.setTime(Instant.ofEpochSecond(1490996779L)); + searchRepository.saveAll(Arrays.asList(search12, search16a, search16b, search23)); + + List result = stats.countPerHourOfDay("SEARCH", new StatsRequest(search12.getTime().minus(10, ChronoUnit.DAYS), search12.getTime().plus(10, ChronoUnit.DAYS), true)); + assertThat(result).hasSize(24); + assertThat(result.get(12).getCount()).isEqualTo(Integer.valueOf(1)); + assertThat(result.get(16).getCount()).isEqualTo(Integer.valueOf(2)); + assertThat(result.get(23).getCount()).isEqualTo(Integer.valueOf(1)); + } + + @Test + void shouldCalculateIndexerApiAccessStats() throws Exception { + IndexerApiAccessEntity apiAccess1 = new IndexerApiAccessEntity(indexer1); + apiAccess1.setResult(IndexerAccessResult.CONNECTION_ERROR); //Counted as failed + apiAccess1.setTime(Instant.now().minus(24, ChronoUnit.HOURS)); + apiAccessRepository.save(apiAccess1); + + IndexerApiAccessEntity apiAccess2 = new IndexerApiAccessEntity(indexer1); + apiAccess2.setResult(IndexerAccessResult.HYDRA_ERROR); //Neither counted as successful nor as failed + apiAccessRepository.save(apiAccess2); + + IndexerApiAccessEntity apiAccess3 = new IndexerApiAccessEntity(indexer1); + apiAccess3.setResult(IndexerAccessResult.SUCCESSFUL); //Counted as successful + apiAccessRepository.save(apiAccess3); + + //Initially ignored by set time period + IndexerApiAccessEntity apiAccess4 = new IndexerApiAccessEntity(indexer1); + apiAccess4.setResult(IndexerAccessResult.SUCCESSFUL); //Counted as successful + apiAccess4.setTime(Instant.now().minus(14, ChronoUnit.DAYS)); + apiAccessRepository.save(apiAccess4); + + List result = stats.indexerApiAccesses(new StatsRequest(Instant.now().minus(10, ChronoUnit.DAYS), Instant.now().plus(10, ChronoUnit.DAYS), false)); + assertThat(result).hasSize(1); + //One yesterday, two today: 1.5 on average + assertThat(result.get(0).getAverageAccessesPerDay()).isCloseTo(1.5D, within(0D)); + //One with connection error, one with another error, one successful + assertThat(result.get(0).getPercentSuccessful()).isCloseTo(33D, within(1D)); + + result = stats.indexerApiAccesses(new StatsRequest(Instant.now().minus(20, ChronoUnit.DAYS), Instant.now().plus(10, ChronoUnit.DAYS), false)); + assertThat(result).hasSize(1); + //One yesterday, two today, one 14 days ago: 4/3=1.33 on average + assertThat(result.get(0).getAverageAccessesPerDay()).isCloseTo(1.33D, within(0.01D)); + //One with connection error, one with another error, two successful + assertThat(result.get(0).getPercentSuccessful()).isCloseTo(50D, within(0D)); + + //Now include diabled + result = stats.indexerApiAccesses(new StatsRequest(Instant.now().minus(10, ChronoUnit.DAYS), Instant.now().plus(10, ChronoUnit.DAYS), true)); + assertThat(result).hasSize(2); + } + + + +} diff --git a/core/src/test/java/org/nzbhydra/indexers/AnizbTest.java b/core/src/test/java/org/nzbhydra/indexers/AnizbTest.java index ff1c77d52..c2abe7158 100644 --- a/core/src/test/java/org/nzbhydra/indexers/AnizbTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/AnizbTest.java @@ -1,7 +1,7 @@ package org.nzbhydra.indexers; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -9,17 +9,17 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.springframework.web.util.UriComponentsBuilder; import java.util.Arrays; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -32,9 +32,9 @@ public class AnizbTest { private QueryGenerator queryGeneratorMock; @InjectMocks - private Anizb testee = new Anizb(); + private Anizb testee = new Anizb(configProviderMock, null, null, null, null, null, null, null, null, null, null, queryGeneratorMock, null, null); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(configProviderMock.getBaseConfig()).thenReturn(baseConfig); @@ -55,32 +55,34 @@ public class AnizbTest { } @Test - public void shouldParseXml() { + void shouldParseXml() { } @Test - public void shouldBuildSimpleQuery() throws IndexerSearchAbortedException { + void shouldBuildSimpleQuery() throws IndexerSearchAbortedException { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("query"); UriComponentsBuilder builder = testee.buildSearchUrl(searchRequest, 0, 100); - assertThat(builder.toUriString(), is("https://anizb.org/api/?q=query")); + assertThat(builder.toUriString()).isEqualTo("https://anizb.org/api/?q=query"); } @Test - public void shouldAddRequiredWords() throws IndexerSearchAbortedException { + void shouldAddRequiredWords() throws IndexerSearchAbortedException { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.getInternalData().setRequiredWords(Arrays.asList("a", "b")); searchRequest.setQuery("query"); UriComponentsBuilder builder = testee.buildSearchUrl(searchRequest, 0, 100); - assertThat(builder.toUriString(), is("https://anizb.org/api/?q=query%20a%20b")); + assertThat(builder.toUriString()).isEqualTo("https://anizb.org/api/?q=query%20a%20b"); } - @Test(expected = IndexerSearchAbortedException.class) - public void shouldAbortIfSearchNotPossible() throws IndexerSearchAbortedException { - SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); - testee.buildSearchUrl(searchRequest, 0, 100); + @Test + void shouldAbortIfSearchNotPossible() throws IndexerSearchAbortedException { + assertThrows(IndexerSearchAbortedException.class, () -> { + SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); + testee.buildSearchUrl(searchRequest, 0, 100); + }); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/indexers/BinsearchTest.java b/core/src/test/java/org/nzbhydra/indexers/BinsearchTest.java index 9ed2b0d0d..13f952abd 100644 --- a/core/src/test/java/org/nzbhydra/indexers/BinsearchTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/BinsearchTest.java @@ -2,9 +2,9 @@ package org.nzbhydra.indexers; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import net.jodah.failsafe.FailsafeException; -import org.junit.Before; -import org.junit.Test; +import dev.failsafe.FailsafeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -13,15 +13,15 @@ import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; import org.nzbhydra.searching.CategoryProvider; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; @@ -30,9 +30,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -50,9 +49,10 @@ public class BinsearchTest { private QueryGenerator queryGeneratorMock; @InjectMocks - private Binsearch testee = new Binsearch(); + private Binsearch testee = new Binsearch(configProviderMock, null, null, null, null, null, + null, null, categoryProviderMock, null, null, queryGeneratorMock, null, null); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(configProviderMock.getBaseConfig()).thenReturn(baseConfig); @@ -70,31 +70,31 @@ public class BinsearchTest { } @Test - public void shouldParseResultsCorrectly() throws Exception { + void shouldParseResultsCorrectly() throws Exception { String html = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/binsearch.html"), Charsets.UTF_8); List searchResultItems = testee.getSearchResultItems(html, new SearchRequest()); - assertThat(searchResultItems.size(), is(1)); + assertThat(searchResultItems.size()).isEqualTo(1); SearchResultItem item = searchResultItems.get(0); - assertThat(item.getTitle(), is("testtitle. 3D.TOPBOT.TrueFrench.1080p.X264.AC3.5.1-JKF.mkv")); - assertThat(item.getLink(), is("https://www.binsearch.info/?action=nzb&176073735=1")); - assertThat(item.getDetails(), is("https://www.binsearch.info/?b=testtitle1.3D.TOPBOT.TrueFrench.1080p.X264.A&g=alt.binaries.movies.mkv&p=Ramer%40marmer.com+%28Clown_nez%29&max=250")); - assertThat(item.getSize(), is(12209999872L)); //12.21 GB = 12.21 * 1000*1000*1000 - assertThat(item.getIndexerGuid(), is("176073735")); - assertThat(item.getPubDate(), is(Instant.ofEpochSecond(1443312000))); - assertThat(item.isAgePrecise(), is(false)); - assertThat(item.getPoster().get(), is("Ramer@marmer.com (Clown_nez)")); - assertThat(item.getGroup().get(), is("alt.binaries.movies.mkv")); + assertThat(item.getTitle()).isEqualTo("testtitle. 3D.TOPBOT.TrueFrench.1080p.X264.AC3.5.1-JKF.mkv"); + assertThat(item.getLink()).isEqualTo("https://www.binsearch.info/?action=nzb&176073735=1"); + assertThat(item.getDetails()).isEqualTo("https://www.binsearch.info/?b=testtitle1.3D.TOPBOT.TrueFrench.1080p.X264.A&g=alt.binaries.movies.mkv&p=Ramer%40marmer.com+%28Clown_nez%29&max=250"); + assertThat(item.getSize()).isEqualTo(12209999872L); //12.21 GB = 12.21 * 1000*1000*1000 + assertThat(item.getIndexerGuid()).isEqualTo("176073735"); + assertThat(item.getPubDate()).isEqualTo(Instant.ofEpochSecond(1443312000)); + assertThat(item.isAgePrecise()).isEqualTo(false); + assertThat(item.getPoster().get()).isEqualTo("Ramer@marmer.com (Clown_nez)"); + assertThat(item.getGroup().get()).isEqualTo("alt.binaries.movies.mkv"); } @Test - public void shouldParseOtherResultsCorrectly() throws Exception { + void shouldParseOtherResultsCorrectly() throws Exception { String html = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/binsearch_randm.html"), Charsets.UTF_8); List searchResultItems = testee.getSearchResultItems(html, new SearchRequest()); - assertThat(searchResultItems.size(), is(41)); + assertThat(searchResultItems.size()).isEqualTo(41); } @Test - public void shouldRecognizeIfSingleResultPage() throws Exception { + void shouldRecognizeIfSingleResultPage() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); String html = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/binsearch_singlepage.html"), Charsets.UTF_8); IndexerSearchResult indexerSearchResult = new IndexerSearchResult(testee, ""); @@ -106,74 +106,79 @@ public class BinsearchTest { } indexerSearchResult.setSearchResultItems(items); testee.completeIndexerSearchResult(html, indexerSearchResult, null, searchRequest, 0, 100); - assertThat(indexerSearchResult.getOffset(), is(0)); - assertThat(indexerSearchResult.getPageSize(), is(100)); - assertThat(indexerSearchResult.getTotalResults(), is(24)); - assertThat(indexerSearchResult.isTotalResultsKnown(), is(true)); - assertThat(indexerSearchResult.isHasMoreResults(), is(false)); + assertThat(indexerSearchResult.getOffset()).isEqualTo(0); + assertThat(indexerSearchResult.getPageSize()).isEqualTo(100); + assertThat(indexerSearchResult.getTotalResults()).isEqualTo(24); + assertThat(indexerSearchResult.isTotalResultsKnown()).isEqualTo(true); + assertThat(indexerSearchResult.isHasMoreResults()).isEqualTo(false); } @Test - public void shouldRecognizeIfMoreResultsAvailable() throws Exception { + void shouldRecognizeIfMoreResultsAvailable() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); String html = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/binsearch.html"), Charsets.UTF_8); IndexerSearchResult indexerSearchResult = new IndexerSearchResult(testee, ""); testee.completeIndexerSearchResult(html, indexerSearchResult, null, searchRequest, 0, 100); - assertThat(indexerSearchResult.isTotalResultsKnown(), is(false)); - assertThat(indexerSearchResult.isHasMoreResults(), is(true)); + assertThat(indexerSearchResult.isTotalResultsKnown()).isEqualTo(false); + assertThat(indexerSearchResult.isHasMoreResults()).isEqualTo(true); } @Test - public void shouldRecognizeWhenNoResultsFound() throws Exception { + void shouldRecognizeWhenNoResultsFound() throws Exception { String html = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/binsearch_noresults.html"), Charsets.UTF_8); List searchResultItems = testee.getSearchResultItems(html, new SearchRequest()); - assertThat(searchResultItems, is(empty())); + assertThat(searchResultItems).isEmpty(); } @Test - public void shouldBuildSimpleQuery() throws IndexerSearchAbortedException { + void shouldBuildSimpleQuery() throws IndexerSearchAbortedException { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("query"); UriComponentsBuilder builder = testee.buildSearchUrl(searchRequest, 0, 100); - assertThat(builder.toUriString(), is("https://www.binsearch.info/?adv_col=on&postdate=date&adv_sort=date&min=0&max=100&q=query")); + assertThat(builder.toUriString()).isEqualTo("https://www.binsearch.info/?adv_col=on&postdate=date&adv_sort=date&min=0&max=100&q=query"); } @Test - public void shouldAddRequiredWords() throws IndexerSearchAbortedException { + void shouldAddRequiredWords() throws IndexerSearchAbortedException { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.getInternalData().setRequiredWords(Arrays.asList("a", "b")); searchRequest.setQuery("query"); UriComponentsBuilder builder = testee.buildSearchUrl(searchRequest, 0, 100); - assertThat(builder.build().toString(), is("https://www.binsearch.info/?adv_col=on&postdate=date&adv_sort=date&min=0&max=100&q=query a b")); - } - - @Test(expected = IndexerSearchAbortedException.class) - public void shouldAbortIfSearchNotPossible() throws IndexerSearchAbortedException { - SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); - testee.buildSearchUrl(searchRequest, 0, 100); + assertThat(builder.build().toString()).isEqualTo("https://www.binsearch.info/?adv_col=on&postdate=date&adv_sort=date&min=0&max=100&q=query a b"); } @Test - public void shouldGetAndParseNfo() throws Exception { + void shouldAbortIfSearchNotPossible() throws IndexerSearchAbortedException { + assertThrows(IndexerSearchAbortedException.class, () -> { + SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); + testee.buildSearchUrl(searchRequest, 0, 100); + }); + } + + @Test + void shouldGetAndParseNfo() throws Exception { String nfoHtml = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/binsearch_nfo.html"), Charsets.UTF_8); testee = spy(testee); doReturn(nfoHtml).when(testee).getAndStoreResultToDatabase(uriCaptor.capture(), any(), any()); NfoResult nfoResult = testee.getNfo("1234"); - assertThat(nfoResult.isHasNfo(), is(true)); - assertThat(nfoResult.getContent(), is("nfocontent")); - assertThat(uriCaptor.getValue().toString(), is("https://www.binsearch.info/viewNFO.php?oid=1234")); + assertThat(nfoResult.isHasNfo()).isEqualTo(true); + assertThat(nfoResult.getContent()).isEqualTo("nfocontent"); + assertThat(uriCaptor.getValue().toString()).isEqualTo("https://www.binsearch.info/viewNFO.php?oid=1234"); } - @Test(expected = FailsafeException.class) - public void shouldRetryOn503() throws Exception { - testee = spy(testee); + @Test + void shouldRetryOn503() throws Exception { + assertThrows(FailsafeException.class, () -> { + testee = spy(testee); // doReturn(nfoHtml).when(testee).getAndStoreResultToDatabase(uriCaptor.capture(), any(), any()); - doThrow(new IndexerAccessException("503")).when(testee).getAndStoreResultToDatabase(uriCaptor.capture(), any(), any()); + doThrow(new IndexerAccessException("503")).when(testee).getAndStoreResultToDatabase(uriCaptor.capture(), any(), any()); - testee.getAndStoreResultToDatabase(null, IndexerApiAccessType.NFO); + testee.getAndStoreResultToDatabase(null, IndexerApiAccessType.NFO); + + }); } diff --git a/core/src/test/java/org/nzbhydra/indexers/DevIndexerTest.java b/core/src/test/java/org/nzbhydra/indexers/DevIndexerTest.java index e219dda6d..356454075 100644 --- a/core/src/test/java/org/nzbhydra/indexers/DevIndexerTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/DevIndexerTest.java @@ -16,7 +16,7 @@ package org.nzbhydra.indexers; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; import org.nzbhydra.mapping.newznab.xml.Xml; @@ -26,14 +26,14 @@ import java.net.URI; public class DevIndexerTest { @InjectMocks - private DevIndexer testee = new DevIndexer(); - + private DevIndexer testee = new DevIndexer(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + @Test - public void testGeneration() throws Exception { + void testGeneration() throws Exception { Xml xml = testee.getAndStoreResultToDatabase(URI.create("http://127.0.01/duplicatesandtitlegroups"), null); NewznabXmlRoot root = (NewznabXmlRoot) xml; System.out.println(root); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/indexers/IndexerStatusesCleanupTaskTest.java b/core/src/test/java/org/nzbhydra/indexers/IndexerStatusesCleanupTaskTest.java index 298dd7288..ab8379442 100644 --- a/core/src/test/java/org/nzbhydra/indexers/IndexerStatusesCleanupTaskTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/IndexerStatusesCleanupTaskTest.java @@ -16,12 +16,13 @@ package org.nzbhydra.indexers; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.ConfigReaderWriter; import org.nzbhydra.config.indexer.IndexerConfig; @@ -41,6 +42,8 @@ public class IndexerStatusesCleanupTaskTest { private BaseConfig baseConfig; @Mock private ConfigReaderWriter configReaderWriterMock; + @Mock + private BaseConfigHandler baseConfigHandler; IndexerConfig indexerConfigEnabled = new IndexerConfig(); IndexerConfig indexerConfigDisabledSystem = new IndexerConfig(); @@ -52,10 +55,10 @@ public class IndexerStatusesCleanupTaskTest { private IndexerStatusesCleanupTask testee; - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - testee = new IndexerStatusesCleanupTask(configProvider); + testee = new IndexerStatusesCleanupTask(configProvider, baseConfigHandler); indexerConfigEnabled.setState(IndexerConfig.State.ENABLED); indexerConfigUserDisabled.setState(IndexerConfig.State.DISABLED_USER); indexerConfigDisabledSystem.setState(IndexerConfig.State.DISABLED_SYSTEM); @@ -75,7 +78,7 @@ public class IndexerStatusesCleanupTaskTest { } @Test - public void shouldCleanup() { + void shouldCleanup() { testee.cleanup(); //Was reenabled @@ -93,4 +96,4 @@ public class IndexerStatusesCleanupTaskTest { assertThat(indexerConfigUserDisabled.getState()).isEqualTo(IndexerConfig.State.DISABLED_USER); assertThat(indexerConfigDisabledSystem.getState()).isEqualTo(IndexerConfig.State.DISABLED_SYSTEM); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/indexers/IndexerTest.java b/core/src/test/java/org/nzbhydra/indexers/IndexerTest.java index ec0cd7f4b..a98b505c3 100644 --- a/core/src/test/java/org/nzbhydra/indexers/IndexerTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/IndexerTest.java @@ -3,8 +3,8 @@ package org.nzbhydra.indexers; import com.google.common.base.Stopwatch; import com.google.common.collect.HashMultiset; import com.google.common.collect.Sets; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -13,9 +13,14 @@ import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.SearchSourceRestriction; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.indexers.exceptions.IndexerAuthException; import org.nzbhydra.indexers.exceptions.IndexerErrorCodeException; @@ -23,7 +28,6 @@ import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException; import org.nzbhydra.indexers.exceptions.IndexerUnreachableException; import org.nzbhydra.mapping.newznab.xml.NewznabXmlError; import org.nzbhydra.mediainfo.InfoProvider; -import org.nzbhydra.mediainfo.MediaIdType; import org.nzbhydra.mediainfo.MediaInfo; import org.nzbhydra.mediainfo.TvInfo; import org.nzbhydra.searching.SearchResultAcceptor; @@ -32,9 +36,7 @@ import org.nzbhydra.searching.db.SearchResultEntity; import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; @@ -52,22 +54,19 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.IntStream; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentMatchers.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; public class IndexerTest { private BaseConfig baseConfig; - @Mock - private IndexerEntity indexerEntityMock; + private IndexerEntity indexerEntityMock = new IndexerEntity("indexerName"); private IndexerConfig indexerConfig = new IndexerConfig(); @Mock private Indexer indexerMock; - @Mock - private SearchResultEntity searchResultEntityMock; + + private SearchResultEntity searchResultEntityMock = new SearchResultEntity(indexerEntityMock, Instant.now(), "title", "guid", "link", "details", DownloadType.NZB, Instant.now()); @Mock private IndexerRepository indexerRepositoryMock; @Mock @@ -98,6 +97,8 @@ public class IndexerTest { private InfoProvider infoProviderMock; @Mock private QueryGenerator queryGeneratorMock; + @Mock + private BaseConfigHandler baseConfigHandler; private List searchResultItemsToReturn = Collections.emptyList(); @@ -140,13 +141,13 @@ public class IndexerTest { }; } - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(indexerMock.getIndexerEntity()).thenReturn(indexerEntityMock); when(indexerMock.getConfig()).thenReturn(indexerConfig); - when(indexerEntityMock.getName()).thenReturn("indexerName"); + when(indexerMock.getName()).thenReturn("indexerName"); testee.indexer = indexerEntityMock; @@ -177,7 +178,7 @@ public class IndexerTest { } @Test - public void shouldCreateNewSearchResultEntityWhenNoneIsFound() throws Exception { + void shouldCreateNewSearchResultEntityWhenNoneIsFound() throws Exception { SearchResultItem item = new SearchResultItem(); item.setIndexer(indexerMock); item.setTitle("title"); @@ -189,18 +190,18 @@ public class IndexerTest { verify(searchResultRepositoryMock).saveAll(searchResultEntitiesCaptor.capture()); List persistedEntities = searchResultEntitiesCaptor.getValue(); - assertThat(persistedEntities.size(), is(1)); - assertThat(persistedEntities.get(0).getTitle(), is("title")); - assertThat(persistedEntities.get(0).getDetails(), is("details")); - assertThat(persistedEntities.get(0).getIndexerGuid(), is("guid")); + assertThat(persistedEntities.size()).isEqualTo(1); + assertThat(persistedEntities.get(0).getTitle()).isEqualTo("title"); + assertThat(persistedEntities.get(0).getDetails()).isEqualTo("details"); + assertThat(persistedEntities.get(0).getIndexerGuid()).isEqualTo("guid"); } @Test - public void shouldNotCreateNewSearchResultEntityWhenOneExists() throws Exception { + void shouldNotCreateNewSearchResultEntityWhenOneExists() throws Exception { SearchResultItem item = new SearchResultItem(); item.setIndexerGuid("guid"); item.setIndexer(indexerMock); - when(searchResultEntityMock.getIndexerGuid()).thenReturn("guid"); + searchResultEntityMock.setIndexerGuid("guid"); when(searchResultRepositoryMock.findAllIdsByIdIn(anyList())).thenReturn(Sets.newHashSet(299225959498991027L)); testee.persistSearchResults(Collections.singletonList(item), new IndexerSearchResult()); @@ -208,33 +209,33 @@ public class IndexerTest { verify(searchResultRepositoryMock).saveAll(searchResultEntitiesCaptor.capture()); List persistedEntities = searchResultEntitiesCaptor.getValue(); - assertThat(persistedEntities.size(), is(0)); + assertThat(persistedEntities.size()).isEqualTo(0); } @Test - public void handleSuccess() throws Exception { + void handleSuccess() throws Exception { indexerConfig.setState(IndexerConfig.State.DISABLED_SYSTEM_TEMPORARY); indexerConfig.setDisabledLevel(1); indexerConfig.setDisabledUntil(Instant.now().toEpochMilli()); testee.handleSuccess(IndexerApiAccessType.SEARCH, 0L); - assertThat(indexerConfig.getState(), is(IndexerConfig.State.ENABLED)); - assertThat(indexerConfig.getDisabledLevel(), is(0)); - assertThat(indexerConfig.getDisabledUntil(), is(nullValue())); + assertThat(indexerConfig.getState()).isEqualTo(IndexerConfig.State.ENABLED); + assertThat(indexerConfig.getDisabledLevel()).isEqualTo(0); + assertThat(indexerConfig.getDisabledUntil()).isNull(); } @Test - public void handleFailure() throws Exception { + void handleFailure() throws Exception { indexerConfig.setState(IndexerConfig.State.ENABLED); indexerConfig.setDisabledLevel(0); indexerConfig.setDisabledUntil(null); testee.handleFailure("reason", false, null, null, null); - assertThat(indexerConfig.getState(), is(IndexerConfig.State.DISABLED_SYSTEM_TEMPORARY)); - assertThat(indexerConfig.getDisabledLevel(), is(1)); + assertThat(indexerConfig.getState()).isEqualTo(IndexerConfig.State.DISABLED_SYSTEM_TEMPORARY); + assertThat(indexerConfig.getDisabledLevel()).isEqualTo(1); long disabledPeriod = Math.abs(Instant.ofEpochMilli(indexerConfig.getDisabledUntil()).getEpochSecond() - Instant.now().getEpochSecond()); long delta = Math.abs(Indexer.DISABLE_PERIODS.get(1) * 60 - disabledPeriod); org.assertj.core.api.Assertions.assertThat(delta).isLessThan(5); @@ -245,31 +246,33 @@ public class IndexerTest { testee.handleFailure("reason", true, null, null, null); - assertThat(indexerConfig.getState(), is(IndexerConfig.State.DISABLED_SYSTEM)); + assertThat(indexerConfig.getState()).isEqualTo(IndexerConfig.State.DISABLED_SYSTEM); } @Test - public void shouldGetAndStoreResultToDatabaseWithSuccess() throws Exception { + void shouldGetAndStoreResultToDatabaseWithSuccess() throws Exception { when(indexerWebAccessMock.get(any(), eq(testee.config), any())).thenReturn("result"); String result = (String) testee.getAndStoreResultToDatabase(new URI("http://127.0.0.1"), String.class, IndexerApiAccessType.SEARCH); - assertThat(result, is("result")); + assertThat(result).isEqualTo("result"); verify(testee).handleSuccess(eq(IndexerApiAccessType.SEARCH), anyLong()); } - @Test(expected = IndexerAccessException.class) - public void shouldGetAndStoreResultToDatabaseWithError() throws Exception { - IndexerAccessException exception = new IndexerAccessException("error"); - when(indexerWebAccessMock.get(any(), eq(testee.config), any())).thenThrow(exception); + @Test + void shouldGetAndStoreResultToDatabaseWithError() throws Exception { + assertThrows(IndexerAccessException.class, () -> { + IndexerAccessException exception = new IndexerAccessException("error"); + when(indexerWebAccessMock.get(any(), eq(testee.config), any())).thenThrow(exception); - testee.getAndStoreResultToDatabase(new URI("http://127.0.0.1"), String.class, IndexerApiAccessType.SEARCH); + testee.getAndStoreResultToDatabase(new URI("http://127.0.0.1"), String.class, IndexerApiAccessType.SEARCH); - verify(testee).handleIndexerAccessException(exception, IndexerApiAccessType.SEARCH); + verify(testee).handleIndexerAccessException(exception, IndexerApiAccessType.SEARCH); + }); } @Test - public void shouldHandleIndexerAccessException() throws Exception { + void shouldHandleIndexerAccessException() throws Exception { IndexerAccessException exception = new IndexerAuthException("error"); testee.handleIndexerAccessException(exception, IndexerApiAccessType.SEARCH); verify(testee).handleFailure("error", true, IndexerApiAccessType.SEARCH, null, IndexerAccessResult.AUTH_ERROR); @@ -284,7 +287,7 @@ public class IndexerTest { } @Test - public void shouldUseFallback() throws Exception { + void shouldUseFallback() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); Map identifiers = new HashMap<>(); @@ -301,52 +304,52 @@ public class IndexerTest { @Test - public void shouldRemoveTrailing() throws Exception { + void shouldRemoveTrailing() throws Exception { baseConfig.getSearching().setRemoveTrailing(Arrays.asList("trailing1", "trailing2")); String result = testee.cleanUpTitle("abc trailing1 trailing2"); - assertThat(result, is("abc")); + assertThat(result).isEqualTo("abc"); testee.handleNewConfig(null); result = testee.cleanUpTitle("abc trailing1 trailing2 def"); - assertThat(result, is("abc trailing1 trailing2 def")); + assertThat(result).isEqualTo("abc trailing1 trailing2 def"); testee.handleNewConfig(null); result = testee.cleanUpTitle("abc"); - assertThat(result, is("abc")); + assertThat(result).isEqualTo("abc"); testee.handleNewConfig(null); baseConfig.getSearching().setRemoveTrailing(Collections.emptyList()); result = testee.cleanUpTitle("abc"); - assertThat(result, is("abc")); + assertThat(result).isEqualTo("abc"); testee.handleNewConfig(null); baseConfig.getSearching().setRemoveTrailing(Arrays.asList("trailing*")); result = testee.cleanUpTitle("abc trailing1 trailing2"); - assertThat(result, is("abc")); + assertThat(result).isEqualTo("abc"); testee.handleNewConfig(null); baseConfig.getSearching().setRemoveTrailing(Arrays.asList("-obfuscated")); result = testee.cleanUpTitle("abc-obfuscated"); - assertThat(result, is("abc")); + assertThat(result).isEqualTo("abc"); testee.handleNewConfig(null); baseConfig.getSearching().setRemoveTrailing(Arrays.asList("-obfuscated******")); result = testee.cleanUpTitle("abc-obfuscated"); - assertThat(result, is("abc")); + assertThat(result).isEqualTo("abc"); testee.handleNewConfig(null); baseConfig.getSearching().setRemoveTrailing(Arrays.asList("[*]")); result = testee.cleanUpTitle("abc [obfuscated]"); - assertThat(result, is("abc")); + assertThat(result).isEqualTo("abc"); testee.handleNewConfig(null); baseConfig.getSearching().setRemoveTrailing(Arrays.asList("[*]")); result = testee.cleanUpTitle("abc [obfuscated] def"); - assertThat(result, is("abc [obfuscated] def")); + assertThat(result).isEqualTo("abc [obfuscated] def"); } @Test - public void shouldRemoveTrailing2() { + void shouldRemoveTrailing2() { List collect = IntStream.range(1, 100).mapToObj(x -> "trailing" + x + "*********").collect(Collectors.toList()); List all = new ArrayList<>(); all.addAll(collect); diff --git a/core/src/test/java/org/nzbhydra/indexers/IndexerWebAccessTest.java b/core/src/test/java/org/nzbhydra/indexers/IndexerWebAccessTest.java index ad20d84b9..c6f7e9d95 100644 --- a/core/src/test/java/org/nzbhydra/indexers/IndexerWebAccessTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/IndexerWebAccessTest.java @@ -1,7 +1,7 @@ package org.nzbhydra.indexers; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -44,7 +44,7 @@ public class IndexerWebAccessTest { @InjectMocks private IndexerWebAccess testee = new IndexerWebAccess(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); String xml = "\n" + @@ -62,14 +62,14 @@ public class IndexerWebAccessTest { } @Test - public void shouldUseIndexerUserAgent() throws Exception{ + void shouldUseIndexerUserAgent() throws Exception { testee.get(new URI("http://127.0.0.1"), indexerConfig); Map headers = headersCaptor.getValue(); assertThat(headers).contains(entry("User-Agent", "indexerUa")); } @Test - public void shouldUseGlobalUserAgentIfNoIndexerUaIsSet() throws Exception{ + void shouldUseGlobalUserAgentIfNoIndexerUaIsSet() throws Exception { indexerConfig.setUserAgent(null); testee.get(new URI("http://127.0.0.1"), indexerConfig); @@ -79,13 +79,13 @@ public class IndexerWebAccessTest { } @Test - public void shouldUseIndexerTimeout() throws Exception{ + void shouldUseIndexerTimeout() throws Exception { testee.get(new URI("http://127.0.0.1"), indexerConfig); assertThat(timeoutCaptor.getValue()).isEqualTo(10); } @Test - public void shouldUseGlobalTimeoutNoIndexerTimeoutIsSet() throws Exception{ + void shouldUseGlobalTimeoutNoIndexerTimeoutIsSet() throws Exception { indexerConfig.setTimeout(null); testee.get(new URI("http://127.0.0.1"), indexerConfig); diff --git a/core/src/test/java/org/nzbhydra/indexers/NewznabTest.java b/core/src/test/java/org/nzbhydra/indexers/NewznabTest.java index a55924067..72b9bd400 100644 --- a/core/src/test/java/org/nzbhydra/indexers/NewznabTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/NewznabTest.java @@ -1,9 +1,10 @@ package org.nzbhydra.indexers; +import com.google.common.base.Stopwatch; import com.google.common.collect.HashMultiset; import com.google.common.collect.Lists; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -11,16 +12,22 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.nzbhydra.NzbHydraException; import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.BaseConfigHandler; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.SearchSourceRestriction; import org.nzbhydra.config.SearchingConfig; import org.nzbhydra.config.category.Category; import org.nzbhydra.config.category.Category.Subtype; +import org.nzbhydra.config.downloading.DownloadType; +import org.nzbhydra.config.indexer.BackendType; import org.nzbhydra.config.indexer.IndexerCategoryConfig.MainCategory; import org.nzbhydra.config.indexer.IndexerCategoryConfig.SubCategory; import org.nzbhydra.config.indexer.IndexerConfig; -import org.nzbhydra.indexers.Indexer.BackendType; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.indexers.exceptions.IndexerAuthException; import org.nzbhydra.indexers.exceptions.IndexerErrorCodeException; @@ -40,7 +47,6 @@ import org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; import org.nzbhydra.mapping.newznab.xml.Xml; import org.nzbhydra.mediainfo.InfoProvider; -import org.nzbhydra.mediainfo.MediaIdType; import org.nzbhydra.mediainfo.MediaInfo; import org.nzbhydra.searching.CategoryProvider; import org.nzbhydra.searching.SearchResultAcceptor; @@ -48,12 +54,9 @@ import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult; import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.HasNfo; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.InternalData.FallbackState; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.springframework.oxm.Unmarshaller; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; @@ -65,11 +68,8 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.eq; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @SuppressWarnings("ALL") @@ -80,8 +80,8 @@ public class NewznabTest { private InfoProvider infoProviderMock; @Mock private IndexerWebAccess indexerWebAccessMock; - @Mock - private IndexerEntity indexerEntityMock; + + private IndexerEntity indexerEntityMock = new IndexerEntity("indexer"); @Mock private CategoryProvider categoryProviderMock; @Mock @@ -109,6 +109,8 @@ public class NewznabTest { @Mock BaseConfig baseConfigMock; @Mock + private BaseConfigHandler baseConfigHandler; + @Mock SearchingConfig searchingConfigMock; @Mock private QueryGenerator queryGeneratorMock; @@ -119,10 +121,11 @@ public class NewznabTest { private ConfigProvider configProviderMock; @InjectMocks - private Newznab testee = new Newznab(); + private Newznab testee = new Newznab(configProviderMock, indexerRepositoryMock, searchResultRepositoryMock, indexerApiAccessRepositoryMock, shortRepositoryMock, null, indexerWebAccessMock, resultAcceptorMock, + categoryProviderMock, infoProviderMock, null, queryGeneratorMock, null, unmarshallerMock, null); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); testee = spy(testee); @@ -142,6 +145,7 @@ public class NewznabTest { testee.config = new IndexerConfig(); testee.config.setSupportedSearchIds(Lists.newArrayList(MediaIdType.TMDB, MediaIdType.TVRAGE)); testee.config.setHost("http://127.0.0.1:1234"); + testee.indexer = indexerEntityMock; baseConfig = new BaseConfig(); when(configProviderMock.getBaseConfig()).thenReturn(baseConfig); @@ -174,26 +178,26 @@ public class NewznabTest { } @Test - public void shouldReturnCorrectSearchResults() throws Exception { + void shouldReturnCorrectSearchResults() throws Exception { NewznabXmlRoot root = RssBuilder.builder().items(Arrays.asList(RssItemBuilder.builder("title").build())).newznabResponse(0, 1).build(); when(indexerWebAccessMock.get(any(), eq(testee.config), any())).thenReturn(root); IndexerSearchResult indexerSearchResult = testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); - assertThat(indexerSearchResult.getSearchResultItems().size(), is(1)); - assertThat(indexerSearchResult.getTotalResults(), is(1)); - assertThat(indexerSearchResult.isHasMoreResults(), is(false)); - assertThat(indexerSearchResult.isTotalResultsKnown(), is(true)); + assertThat(indexerSearchResult.getSearchResultItems().size()).isEqualTo(1); + assertThat(indexerSearchResult.getTotalResults()).isEqualTo(1); + assertThat(indexerSearchResult.isHasMoreResults()).isEqualTo(false); + assertThat(indexerSearchResult.isTotalResultsKnown()).isEqualTo(true); } @Test - public void shouldAccountForRejectedResults() throws Exception { + void shouldAccountForRejectedResults() throws Exception { List items = Arrays.asList( - RssItemBuilder.builder("title1").build(), - RssItemBuilder.builder("title2").build(), - RssItemBuilder.builder("title3").build(), - RssItemBuilder.builder("title4").build(), - RssItemBuilder.builder("title5").build() + RssItemBuilder.builder("title1").build(), + RssItemBuilder.builder("title2").build(), + RssItemBuilder.builder("title3").build(), + RssItemBuilder.builder("title4").build(), + RssItemBuilder.builder("title5").build() ); NewznabXmlRoot root = RssBuilder.builder().items(items).newznabResponse(100, 105).build(); when(indexerWebAccessMock.get(any(), eq(testee.config), any())).thenReturn(root); @@ -211,14 +215,14 @@ public class NewznabTest { IndexerSearchResult indexerSearchResult = testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); - assertThat(indexerSearchResult.getSearchResultItems().size(), is(3)); - assertThat(indexerSearchResult.getTotalResults(), is(105)); - assertThat(indexerSearchResult.isHasMoreResults(), is(false)); - assertThat(indexerSearchResult.isTotalResultsKnown(), is(true)); + assertThat(indexerSearchResult.getSearchResultItems().size()).isEqualTo(3); + assertThat(indexerSearchResult.getTotalResults()).isEqualTo(105); + assertThat(indexerSearchResult.isHasMoreResults()).isEqualTo(false); + assertThat(indexerSearchResult.isTotalResultsKnown()).isEqualTo(true); } @Test - public void shouldGetIdsIfNoneOfTheProvidedAreSupported() throws Exception { + void shouldGetIdsIfNoneOfTheProvidedAreSupported() throws Exception { when(infoProviderMock.canConvertAny(anySet(), anySet())).thenReturn(true); SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); @@ -230,14 +234,14 @@ public class NewznabTest { builder = testee.extendQueryUrlWithSearchIds(searchRequest, builder); MultiValueMap params = builder.build().getQueryParams(); - assertFalse(params.containsKey("imdbid")); - assertFalse(params.containsKey("tmdbid")); - assertFalse(params.containsKey("rid")); + assertThat(params.containsKey("imdbid")).isFalse(); + assertThat(params.containsKey("tmdbid")).isFalse(); + assertThat(params.containsKey("rid")).isFalse(); assertTrue(params.containsKey("tvmazeid")); } @Test - public void shouldNotGetInfosIfAtLeastOneProvidedIsSupported() throws Exception { + void shouldNotGetInfosIfAtLeastOneProvidedIsSupported() throws Exception { testee.config = new IndexerConfig(); testee.config.setSupportedSearchIds(Lists.newArrayList(MediaIdType.IMDB)); SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); @@ -247,12 +251,12 @@ public class NewznabTest { builder = testee.extendQueryUrlWithSearchIds(searchRequest, builder); MultiValueMap params = builder.build().getQueryParams(); assertTrue(params.containsKey("imdbid")); - assertEquals(1, params.size()); + assertThat(params).hasSize(1); verify(infoProviderMock, never()).convert(anyString(), any(MediaIdType.class)); } @Test - public void shouldRemoveTrailingTtFromImdbId() throws Exception { + void shouldRemoveTrailingTtFromImdbId() throws Exception { testee.config = new IndexerConfig(); testee.config.setSupportedSearchIds(Lists.newArrayList(MediaIdType.IMDB)); SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); @@ -262,8 +266,8 @@ public class NewznabTest { builder = testee.extendQueryUrlWithSearchIds(searchRequest, builder); MultiValueMap params = builder.build().getQueryParams(); assertTrue(params.containsKey("imdbid")); - assertEquals(1, params.size()); - assertEquals("12345", params.get("imdbid").get(0)); + assertThat(params).hasSize(1); + assertThat(params.get("imdbid").get(0)).isEqualTo("12345"); verify(infoProviderMock, never()).convert(anyString(), any(MediaIdType.class)); } @@ -301,13 +305,13 @@ public class NewznabTest { mediaInfo.setTitle("someShow"); when(infoProviderMock.convert("tvdbId", MediaIdType.TVDB)).thenReturn(mediaInfo); - assertEquals("someShow", testee.generateQueryIfApplicable(searchRequest, "")); + assertThat(testee.generateQueryIfApplicable(searchRequest, "")).isEqualTo("someShow"); //Don't add season/episode for fallback queries searchRequest.getInternalData().setFallbackStateByIndexer("indexer", FallbackState.REQUESTED); searchRequest.setSeason(1); searchRequest.setEpisode("1"); - assertEquals("someShow", testee.generateQueryIfApplicable(searchRequest, "")); + assertThat(testee.generateQueryIfApplicable(searchRequest, "")).isEqualTo("someShow"); } // TODO Move to QueryGeneratorTest @@ -326,7 +330,7 @@ public class NewznabTest { } @Test - public void shouldUseAuthorAndTitleIfBookSearchSupported() throws Exception { + void shouldUseAuthorAndTitleIfBookSearchSupported() throws Exception { testee.config = new IndexerConfig(); baseConfig.getSearching().setGenerateQueries(SearchSourceRestriction.BOTH); testee.config.setHost("http://www.indexer.com"); @@ -335,48 +339,56 @@ public class NewznabTest { searchRequest.setAuthor("author"); searchRequest.setTitle("title"); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://www.indexer.com/api?t=book&extended=1&title=title&author=author").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://www.indexer.com/api?t=book&extended=1&title=title&author=author&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); } private UriComponentsBuilder buildCleanedSearchUrl(SearchRequest searchRequest, Integer offset, Integer limit) throws IndexerSearchAbortedException { return testee.buildSearchUrl(searchRequest, offset, limit).replaceQueryParam("password").replaceQueryParam("pw"); } - @Test(expected = IndexerAuthException.class) - public void shouldThrowAuthException() throws Exception { - doReturn(new NewznabXmlError("101", "Wrong API key")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); - doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); + @Test + void shouldThrowAuthException() throws Exception { + assertThrows(IndexerAuthException.class, () -> { + doReturn(new NewznabXmlError("101", "Wrong API key")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); + doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); - testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); - } - - @Test(expected = IndexerErrorCodeException.class) - public void shouldThrowErrorCodeWhen100ApiHitLimits() throws Exception { - doReturn(new NewznabXmlError("100", "Daily Hits Limit Reached\"")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); - doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); - - testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); - } - - - @Test(expected = IndexerProgramErrorException.class) - public void shouldThrowProgramErrorCodeException() throws Exception { - doReturn(new NewznabXmlError("200", "Whatever")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); - doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); - - testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); - } - - @Test(expected = IndexerErrorCodeException.class) - public void shouldThrowErrorCodeThatsNotMyFaultException() throws Exception { - doReturn(new NewznabXmlError("123", "Whatever")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); - doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); - - testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); + testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); + }); } @Test - public void shouldConvertIdIfNecessary() throws Exception { + void shouldThrowErrorCodeWhen100ApiHitLimits() throws Exception { + assertThrows(IndexerErrorCodeException.class, () -> { + doReturn(new NewznabXmlError("100", "Daily Hits Limit Reached\"")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); + doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); + + testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); + }); + } + + + @Test + void shouldThrowProgramErrorCodeException() throws Exception { + assertThrows(IndexerProgramErrorException.class, () -> { + doReturn(new NewznabXmlError("200", "Whatever")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); + doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); + + testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); + }); + } + + @Test + void shouldThrowErrorCodeThatsNotMyFaultException() throws Exception { + assertThrows(IndexerErrorCodeException.class, () -> { + doReturn(new NewznabXmlError("123", "Whatever")).when(testee).getAndStoreResultToDatabase(any(), eq(Xml.class), eq(IndexerApiAccessType.SEARCH)); + doNothing().when(testee).handleFailure(errorMessageCaptor.capture(), disabledPermanentlyCaptor.capture(), any(IndexerApiAccessType.class), any(), indexerApiAccessResultCaptor.capture()); + + testee.searchInternal(new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100), 0, 100); + }); + } + + @Test + void shouldConvertIdIfNecessary() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.getIdentifiers().put(MediaIdType.IMDB, "imdbId"); testee.config.getSupportedSearchIds().add(MediaIdType.TMDB); @@ -388,7 +400,7 @@ public class NewznabTest { } @Test - public void shouldNotConvertIdIfNotNecessary() throws Exception { + void shouldNotConvertIdIfNotNecessary() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.getIdentifiers().put(MediaIdType.TMDB, "tmdbId"); @@ -398,73 +410,73 @@ public class NewznabTest { } @Test - public void shouldAddExcludedAndRequiredWordsToQuery() throws Exception { + void shouldAddExcludedAndRequiredWordsToQuery() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("q"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q --a --b --c").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q --a --b --c&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("aquery"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=aquery --a --b --c").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=aquery --a --b --c&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("q"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); searchRequest.getInternalData().setRequiredWords(Lists.newArrayList("x", "y", "z")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q x y z --a --b --c").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q x y z --a --b --c&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); searchRequest.getCategory().getForbiddenWords().add("catforbidden"); searchRequest.getCategory().getRequiredWords().add("catrequired"); baseConfig.getSearching().setForbiddenWords(Lists.newArrayList("globalforbidden")); baseConfig.getSearching().setRequiredWords(Lists.newArrayList("globalrequired")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q x y z globalrequired catrequired --a --b --c --globalforbidden --catforbidden").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q x y z globalrequired catrequired --a --b --c --globalforbidden --catforbidden&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); } @Test - public void shoulNotdAddExcludedAndRequiredWordsWithSomeCharacters() throws Exception { + void shoulNotdAddExcludedAndRequiredWordsWithSomeCharacters() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("q"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b b", "-c", "d.d")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q --a").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q --a&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); } @Test - public void shouldUseDifferentExclusionFormatForNzedbAndOmgWtf() throws Exception { + void shouldUseDifferentExclusionFormatForNzedbAndOmgWtf() throws Exception { testee.config.setBackend(BackendType.NZEDB); SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("q"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q !a,!b,!c").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=q !a,!b,!c&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("aquery"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=aquery !a,!b,!c").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&q=aquery !a,!b,!c&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); testee.config.setBackend(BackendType.NEWZNAB); testee.config.setHost("http://www.OMGwtfnzbs.com"); searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("q"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://www.OMGwtfnzbs.com/api?t=search&extended=1&q=q !a,!b,!c").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://www.OMGwtfnzbs.com/api?t=search&extended=1&q=q !a,!b,!c&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.setQuery("aquery"); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://www.OMGwtfnzbs.com/api?t=search&extended=1&q=aquery !a,!b,!c").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://www.OMGwtfnzbs.com/api?t=search&extended=1&q=aquery !a,!b,!c&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); } @Test - public void shouldNotAddForbiddenWordsToEmptyQuery() throws Exception { + void shouldNotAddForbiddenWordsToEmptyQuery() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.getInternalData().setForbiddenWords(Lists.newArrayList("a", "b", "c")); - assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); + assertEquals(UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:1234/api?t=search&extended=1&limit=1000").build(), buildCleanedSearchUrl(searchRequest, null, null).build()); } @Test - public void shouldCreateSearchResultItem() throws Exception { + void shouldCreateSearchResultItem() throws Exception { NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.getNewznabAttributes().add(new NewznabAttribute("password", "0")); rssItem.getNewznabAttributes().add(new NewznabAttribute("group", "group")); @@ -477,23 +489,23 @@ public class NewznabTest { rssItem.getNewznabAttributes().add(new NewznabAttribute("category", "5050")); SearchResultItem item = testee.createSearchResultItem(rssItem); - assertThat(item.getLink(), is("http://indexer.com/nzb/123")); - assertThat(item.getIndexerGuid(), is("123")); - assertThat(item.getSize(), is(456L)); - assertThat(item.getDescription(), is("description")); - assertThat(item.getPubDate(), is(Instant.ofEpochSecond(5555555))); - assertThat(item.getCommentsLink(), is("http://indexer.com/details/123x#comments")); - assertThat(item.getDetails(), is("http://indexer.com/details/123")); - assertThat(item.isAgePrecise(), is(true)); - assertThat(item.getUsenetDate().get(), is(Instant.ofEpochSecond(6666666))); - assertThat(item.getDownloadType(), is(DownloadType.NZB)); + assertThat(item.getLink()).isEqualTo("http://indexer.com/nzb/123"); + assertThat(item.getIndexerGuid()).isEqualTo("123"); + assertThat(item.getSize()).isEqualTo(456L); + assertThat(item.getDescription()).isEqualTo("description"); + assertThat(item.getPubDate()).isEqualTo(Instant.ofEpochSecond(5555555)); + assertThat(item.getCommentsLink()).isEqualTo("http://indexer.com/details/123x#comments"); + assertThat(item.getDetails()).isEqualTo("http://indexer.com/details/123"); + assertThat(item.isAgePrecise()).isEqualTo(true); + assertThat(item.getUsenetDate().get()).isEqualTo(Instant.ofEpochSecond(6666666)); + assertThat(item.getDownloadType()).isEqualTo(DownloadType.NZB); - assertThat(item.isPassworded(), is(false)); - assertThat(item.getGroup().get(), is("group")); - assertThat(item.getPoster().get(), is("poster")); - assertThat(item.getFiles(), is(10)); - assertThat(item.getGrabs(), is(20)); - assertThat(item.getCommentsCount(), is(30)); + assertThat(item.isPassworded()).isEqualTo(false); + assertThat(item.getGroup().get()).isEqualTo("group"); + assertThat(item.getPoster().get()).isEqualTo("poster"); + assertThat(item.getFiles()).isEqualTo(10); + assertThat(item.getGrabs()).isEqualTo(20); + assertThat(item.getCommentsCount()).isEqualTo(30); verify(categoryProviderMock, times(1)).fromResultNewznabCategories(Arrays.asList(5000, 5050)); rssItem.setRssGuid(new NewznabXmlGuid("123", false)); @@ -501,13 +513,88 @@ public class NewznabTest { rssItem.getNewznabAttributes().add(new NewznabAttribute("password", "1")); rssItem.getNewznabAttributes().add(new NewznabAttribute("nfo", "1")); item = testee.createSearchResultItem(rssItem); - assertThat(item.getIndexerGuid(), is("123")); - assertThat(item.isPassworded(), is(true)); - assertThat(item.getHasNfo(), is(HasNfo.YES)); + assertThat(item.getIndexerGuid()).isEqualTo("123"); + assertThat(item.isPassworded()).isEqualTo(true); + assertThat(item.getHasNfo()).isEqualTo(HasNfo.YES); } @Test - public void shouldUseIndexersCategoryMappingToBuildOriginalCategoryName() throws Exception { + public void shouldParseGuid() throws Exception { + NewznabXmlItem rssItem = buildBasicRssItem(); + rssItem.getRssGuid().setPermaLink(false); + rssItem.getRssGuid().setGuid("123abc"); + SearchResultItem item = testee.createSearchResultItem(rssItem); + assertThat(item.getIndexerGuid()).isEqualTo("123abc"); + + rssItem.getRssGuid().setPermaLink(true); + rssItem.getRssGuid().setGuid("https://hello.com/details?id=123abc"); + item = testee.createSearchResultItem(rssItem); + assertThat(item.getIndexerGuid()).isEqualTo("123abc"); + + rssItem.getRssGuid().setGuid("https://newztown.co.za/details/123abc"); + item = testee.createSearchResultItem(rssItem); + assertThat(item.getIndexerGuid()).isEqualTo("123abc"); + + rssItem.getRssGuid().setGuid("https://newztown.co.za/details/123abc#details"); + item = testee.createSearchResultItem(rssItem); + assertThat(item.getIndexerGuid()).isEqualTo("123abc"); + + rssItem.getRssGuid().setGuid("https://nzbfinder.ws/details/1db2ba7c-0605-4a98-9c56-71da12cbd18c"); + item = testee.createSearchResultItem(rssItem); + assertThat(item.getIndexerGuid()).isEqualTo("1db2ba7c-0605-4a98-9c56-71da12cbd18c"); + + rssItem.getRssGuid().setGuid("https://nzbgeek.info/geekseek.php?guid=66a34818a38ae8aea3dd77e828dae05b"); + item = testee.createSearchResultItem(rssItem); + assertThat(item.getIndexerGuid()).isEqualTo("66a34818a38ae8aea3dd77e828dae05b"); + + rssItem.getRssGuid().setGuid("https://www.tabula-rasa.pw/details/552aeb3c-6617-4741-9218-f15012b7d21c"); + item = testee.createSearchResultItem(rssItem); + assertThat(item.getIndexerGuid()).isEqualTo("552aeb3c-6617-4741-9218-f15012b7d21c"); + } + + @Test + public void shouldPerformance() throws Exception { + + for (int i = 0; i < 1000; i++) { + parse(); + } + final Stopwatch stopwatch = Stopwatch.createStarted(); + for (int i = 0; i < 5000; i++) { + parse(); + } + System.out.println(stopwatch.elapsed()); + + } + + private void parse() throws NzbHydraException { + NewznabXmlItem rssItem = buildBasicRssItem(); + rssItem.getRssGuid().setPermaLink(false); + rssItem.getRssGuid().setGuid("123abc"); + SearchResultItem item = testee.createSearchResultItem(rssItem); + + + rssItem.getRssGuid().setPermaLink(true); + rssItem.getRssGuid().setGuid("https://hello.com/details?id=123abc"); + item = testee.createSearchResultItem(rssItem); + + rssItem.getRssGuid().setGuid("https://newztown.co.za/details/123abc"); + item = testee.createSearchResultItem(rssItem); + + rssItem.getRssGuid().setGuid("https://newztown.co.za/details/123abc#details"); + item = testee.createSearchResultItem(rssItem); + + rssItem.getRssGuid().setGuid("https://nzbfinder.ws/details/1db2ba7c-0605-4a98-9c56-71da12cbd18c"); + item = testee.createSearchResultItem(rssItem); + + rssItem.getRssGuid().setGuid("https://nzbgeek.info/geekseek.php?guid=66a34818a38ae8aea3dd77e828dae05b"); + item = testee.createSearchResultItem(rssItem); + + rssItem.getRssGuid().setGuid("https://www.tabula-rasa.pw/details/552aeb3c-6617-4741-9218-f15012b7d21c"); + item = testee.createSearchResultItem(rssItem); + } + + @Test + void shouldUseIndexersCategoryMappingToBuildOriginalCategoryName() throws Exception { testee.config.getCategoryMapping().setCategories(Arrays.asList(new MainCategory(5000, "TV", Arrays.asList(new SubCategory(5040, "HD"))))); NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.getNewznabAttributes().add(new NewznabAttribute("category", "5040")); @@ -518,12 +605,12 @@ public class NewznabTest { searchResultItem.setCategory(category); testee.parseAttributes(rssItem, searchResultItem); - assertThat(searchResultItem.getOriginalCategory(), is("TV HD")); + assertThat(searchResultItem.getOriginalCategory()).isEqualTo("TV HD"); rssItem.getNewznabAttributes().clear(); rssItem.getNewznabAttributes().add(new NewznabAttribute("category", "1234")); testee.parseAttributes(rssItem, searchResultItem); - assertThat(searchResultItem.getOriginalCategory(), is("N/A")); + assertThat(searchResultItem.getOriginalCategory()).isEqualTo("N/A"); } @@ -541,29 +628,29 @@ public class NewznabTest { } @Test - public void shouldGetDetailsLinkFromRssGuidIfPermalink() throws Exception { + void shouldGetDetailsLinkFromRssGuidIfPermalink() throws Exception { NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.setRssGuid(new NewznabXmlGuid("detailsLink", true)); rssItem.setComments("http://indexer.com/123/detailsfromcomments#comments"); SearchResultItem item = testee.createSearchResultItem(rssItem); - assertThat(item.getDetails(), is("detailsLink")); + assertThat(item.getDetails()).isEqualTo("detailsLink"); } @Test - public void shouldGetDetailsLinkFromCommentsIfNotSetFromRssGuid() throws Exception { + void shouldGetDetailsLinkFromCommentsIfNotSetFromRssGuid() throws Exception { NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.setRssGuid(new NewznabXmlGuid("someguid", false)); rssItem.setComments("http://indexer.com/123/detailsfromcomments#comments"); SearchResultItem item = testee.createSearchResultItem(rssItem); - assertThat(item.getDetails(), is("http://indexer.com/123/detailsfromcomments")); + assertThat(item.getDetails()).isEqualTo("http://indexer.com/123/detailsfromcomments"); } @Test - public void shouldNotSetGroupOrPosterIfNotAvailable() throws Exception { + void shouldNotSetGroupOrPosterIfNotAvailable() throws Exception { NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.getNewznabAttributes().clear(); rssItem.getNewznabAttributes().add(new NewznabAttribute("group", "not available")); @@ -571,47 +658,47 @@ public class NewznabTest { SearchResultItem item = testee.createSearchResultItem(rssItem); - assertThat(item.getGroup().isPresent(), is(false)); - assertThat(item.getPoster().isPresent(), is(false)); + assertThat(item.getGroup().isPresent()).isEqualTo(false); + assertThat(item.getPoster().isPresent()).isEqualTo(false); } @Test - public void shouldReadGroupFromDescription() throws Exception { + void shouldReadGroupFromDescription() throws Exception { NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.setDescription("Group: alt.binaries.tun
              "); - assertThat(testee.createSearchResultItem(rssItem).getGroup().get(), is("alt.binaries.tun")); + assertThat(testee.createSearchResultItem(rssItem).getGroup().get()).isEqualTo("alt.binaries.tun"); } @Test - public void shouldRemoveTrailingWords() throws Exception { + void shouldRemoveTrailingWords() throws Exception { baseConfig.getSearching().setRemoveTrailing(Arrays.asList("English", "-Obfuscated", " spanish")); NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.setTitle("Some title English"); - assertThat(testee.createSearchResultItem(rssItem).getTitle(), is("Some title")); + assertThat(testee.createSearchResultItem(rssItem).getTitle()).isEqualTo("Some title"); rssItem.setTitle("Some title-Obfuscated"); - assertThat(testee.createSearchResultItem(rssItem).getTitle(), is("Some title")); + assertThat(testee.createSearchResultItem(rssItem).getTitle()).isEqualTo("Some title"); rssItem.setTitle("Some title Spanish"); - assertThat(testee.createSearchResultItem(rssItem).getTitle(), is("Some title")); + assertThat(testee.createSearchResultItem(rssItem).getTitle()).isEqualTo("Some title"); } @Test - public void shouldReturnNfoFromRaw() throws Exception { + void shouldReturnNfoFromRaw() throws Exception { doReturn("rawnfo").when(testee).getAndStoreResultToDatabase(any(), eq(String.class), eq(IndexerApiAccessType.NFO)); NfoResult nfo = testee.getNfo("guid"); - assertThat(nfo.getContent(), is("rawnfo")); - assertThat(nfo.isSuccessful(), is(true)); - assertThat(nfo.isHasNfo(), is(true)); + assertThat(nfo.getContent()).isEqualTo("rawnfo"); + assertThat(nfo.isSuccessful()).isEqualTo(true); + assertThat(nfo.isHasNfo()).isEqualTo(true); } @Test - public void shouldParseXml() throws Exception { + void shouldParseXml() throws Exception { doReturn("alt.binaries.hdtv.x264
              1.01 GB
              7 hours
              31 files (1405 parts) by s@nd.p (SP)
              1 NFO | 9 PAR2 | 1 NZB | 19 ARCHIVE - View NFO

              \n" + - "]]>") - .category("alt.binaries.hdtv.x264") - .pubDate(now) - .rssGuid(new NewznabXmlGuid(link, true)) - .enclosure(new NewznabXmlEnclosure(enclosureUrl, 1089197181L, "application/x-nzb")) - .build())).build(); + Arrays.asList( + RssItemBuilder + .builder("Watchers.of.the.Universe.S02E09.1080p.WEB-DL.DD5.1.AAC2.0.H.264-YFN-0030-Watchers.of.the.Universe.S02E09.Cant.Get.You.out.of.My.Head.1080p.WEB-DL") + .link(link) + .description("alt.binaries.hdtv.x264
              1.01 GB
              7 hours
              31 files (1405 parts) by s@nd.p (SP)
              1 NFO | 9 PAR2 | 1 NZB | 19 ARCHIVE - View NFO

              \n" + + "]]>") + .category("alt.binaries.hdtv.x264") + .pubDate(now) + .rssGuid(new NewznabXmlGuid(link, true)) + .enclosure(new NewznabXmlEnclosure(enclosureUrl, 1089197181L, "application/x-nzb")) + .build())).build(); List items = testee.getSearchResultItems(root, new SearchRequest()); - assertThat(items.size(), is(1)); + assertThat(items.size()).isEqualTo(1); SearchResultItem item = items.get(0); - assertThat(item.getTitle(), is("Watchers.of.the.Universe.S02E09.1080p.WEB-DL.DD5.1.AAC2.0.H.264-YFN-0030-Watchers.of.the.Universe.S02E09.Cant.Get.You.out.of.My.Head.1080p.WEB-DL")); - assertThat(item.getGroup().get(), is("alt.binaries.hdtv.x264")); - assertThat(item.getPubDate(), is(now)); - assertThat(item.isAgePrecise(), is(true)); - assertThat(item.getSize(), is(1089197181L)); - assertThat(item.getIndexerGuid(), is("164950363")); - assertThat(item.getDownloadType(), is(DownloadType.NZB)); - assertThat(item.getHasNfo(), is(HasNfo.NO)); + assertThat(item.getTitle()).isEqualTo("Watchers.of.the.Universe.S02E09.1080p.WEB-DL.DD5.1.AAC2.0.H.264-YFN-0030-Watchers.of.the.Universe.S02E09.Cant.Get.You.out.of.My.Head.1080p.WEB-DL"); + assertThat(item.getGroup().get()).isEqualTo("alt.binaries.hdtv.x264"); + assertThat(item.getPubDate()).isEqualTo(now); + assertThat(item.isAgePrecise()).isEqualTo(true); + assertThat(item.getSize()).isEqualTo(1089197181L); + assertThat(item.getIndexerGuid()).isEqualTo("164950363"); + assertThat(item.getDownloadType()).isEqualTo(DownloadType.NZB); + assertThat(item.getHasNfo()).isEqualTo(HasNfo.NO); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/indexers/OkHttpHandshakeAlert.java b/core/src/test/java/org/nzbhydra/indexers/OkHttpHandshakeAlert.java index a555fb36e..6714b7c3d 100644 --- a/core/src/test/java/org/nzbhydra/indexers/OkHttpHandshakeAlert.java +++ b/core/src/test/java/org/nzbhydra/indexers/OkHttpHandshakeAlert.java @@ -2,8 +2,8 @@ package org.nzbhydra.indexers; import okhttp3.OkHttpClient.Builder; import okhttp3.Request; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; @@ -14,7 +14,7 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -@Ignore //Only of OkHTTP bug report, call methods separately. See https://github.com/square/okhttp/issues/3573 +@Disabled //Only of OkHTTP bug report, call methods separately. See https://github.com/square/okhttp/issues/3573 public class OkHttpHandshakeAlert { private final String HOST_DOES_NOT_WORK_WITHOUT_SNI = "https://binsearch.info"; private final String HOST_DOES_NOT_WORK_WITH_SNI = "https://nzbgeek.info"; @@ -24,14 +24,14 @@ public class OkHttpHandshakeAlert { //Caused by RealConnection.java:281 which cannot be skipped / disabled @Test - public void causesSslException() throws Exception { + void causesSslException() throws Exception { Builder builder = getUnsafeOkHttpClientBuilder(new Builder()); builder.build().newCall(new Request.Builder().url(HOST_DOES_NOT_WORK_WITH_SNI).build()).execute(); } @Test - public void doesNotCauseException() throws Exception { + void doesNotCauseException() throws Exception { //Will work with SNI disabled System.setProperty("jsse.enableSNIExtension", "false"); Builder builder = getUnsafeOkHttpClientBuilder(new Builder()); @@ -39,7 +39,7 @@ public class OkHttpHandshakeAlert { } @Test - public void doesNotCauseExceptionForTheOneButForTheOther() throws Exception { + void doesNotCauseExceptionForTheOneButForTheOther() throws Exception { System.setProperty("jsse.enableSNIExtension", "false"); Builder builder = getUnsafeOkHttpClientBuilder(new Builder()); //Will work with SNI disabled @@ -49,7 +49,7 @@ public class OkHttpHandshakeAlert { } @Test - public void worksWithSNI() throws Exception { + void worksWithSNI() throws Exception { System.setProperty("jsse.enableSNIExtension", "true"); //default setting Builder builder = getUnsafeOkHttpClientBuilder(new Builder()); //Will work with SNI enabled diff --git a/core/src/test/java/org/nzbhydra/indexers/SslTest.java b/core/src/test/java/org/nzbhydra/indexers/SslTest.java index 56bcddb13..3325b345d 100644 --- a/core/src/test/java/org/nzbhydra/indexers/SslTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/SslTest.java @@ -1,22 +1,22 @@ package org.nzbhydra.indexers; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.nzbhydra.NzbHydra; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.indexer.IndexerConfig; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import java.net.URI; -@RunWith(SpringRunner.class) +@ExtendWith(SpringExtension.class) @SpringBootTest(classes = NzbHydra.class) -@DataJpaTest -@Ignore //Only run when needed, we don't want any access to internet from build +@AutoConfigureDataJpa +@Disabled //Only run when needed, we don't want any access to internet from build public class SslTest { //Just test that some sites can be visited that used to cause troubles because of SNI @@ -31,7 +31,7 @@ public class SslTest { private ConfigProvider configProvider; @Test - public void shouldBeAbleToAccessSSL() throws Exception { + void shouldBeAbleToAccessSSL() throws Exception { configProvider.getBaseConfig().getMain().setVerifySsl(false); IndexerConfig config = new IndexerConfig(); @@ -47,4 +47,4 @@ public class SslTest { } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/indexers/capscheck/NewznabCheckerTest.java b/core/src/test/java/org/nzbhydra/indexers/capscheck/NewznabCheckerTest.java index d58b4a1cd..662107dbe 100644 --- a/core/src/test/java/org/nzbhydra/indexers/capscheck/NewznabCheckerTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/capscheck/NewznabCheckerTest.java @@ -18,19 +18,20 @@ package org.nzbhydra.indexers.capscheck; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.SearchingConfig; +import org.nzbhydra.config.indexer.BackendType; +import org.nzbhydra.config.indexer.CheckCapsResponse; import org.nzbhydra.config.indexer.IndexerCategoryConfig; import org.nzbhydra.config.indexer.IndexerConfig; import org.nzbhydra.fortests.NewznabResponseBuilder; import org.nzbhydra.indexers.BinsearchTest; -import org.nzbhydra.indexers.Indexer.BackendType; import org.nzbhydra.indexers.IndexerWebAccess; import org.nzbhydra.indexers.exceptions.IndexerAccessException; import org.nzbhydra.mapping.newznab.ActionAttribute; @@ -48,12 +49,12 @@ import javax.xml.transform.stream.StreamSource; import java.io.StringReader; import java.net.URI; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -import static org.nzbhydra.mediainfo.MediaIdType.*; +import static org.nzbhydra.config.mediainfo.MediaIdType.*; @SuppressWarnings("ALL") @@ -80,7 +81,7 @@ public class NewznabCheckerTest { private IndexerConfig indexerConfig; - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); indexerConfig = new IndexerConfig(); @@ -98,20 +99,20 @@ public class NewznabCheckerTest { } @Test - public void shouldCheckCaps() throws Exception { + void shouldCheckCaps() throws Exception { NewznabResponseBuilder builder = new NewznabResponseBuilder(); NewznabXmlRoot thronesResult = builder.getTestResult(1, 100, "Thrones", 0, 100); thronesResult.getRssChannel().setGenerator("nzedb"); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&tvdbid=121361"), indexerConfig)) - .thenReturn(thronesResult); + .thenReturn(thronesResult); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&rid=24493"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&tvmazeid=82"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&imdbid=0944947"), indexerConfig)) .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&traktid=1390"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "GOT", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "GOT", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=movie&tmdbid=24428"), indexerConfig)) .thenReturn(builder.getTestResult(1, 100, "Avengers", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=movie&imdbid=0848228"), indexerConfig)) @@ -120,7 +121,7 @@ public class NewznabCheckerTest { capsRoot.getSearching().setAudioSearch(new CapsXmlSearch("yes", "q")); CheckCapsResponse checkCapsRespone = testee.checkCaps(indexerConfig); - assertEquals(7, checkCapsRespone.getIndexerConfig().getSupportedSearchIds().size()); + assertThat(checkCapsRespone.getIndexerConfig().getSupportedSearchIds()).hasSize(7); assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchIds().contains(TVDB)); assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchIds().contains(TVRAGE)); assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchIds().contains(TVMAZE)); @@ -129,12 +130,12 @@ public class NewznabCheckerTest { assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchIds().contains(TVIMDB)); assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchIds().contains(TMDB)); - assertEquals(3, checkCapsRespone.getIndexerConfig().getSupportedSearchTypes().size()); + assertThat(checkCapsRespone.getIndexerConfig().getSupportedSearchTypes()).hasSize(3); assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchTypes().contains(ActionAttribute.AUDIO)); assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchTypes().contains(ActionAttribute.TVSEARCH)); assertTrue(checkCapsRespone.getIndexerConfig().getSupportedSearchTypes().contains(ActionAttribute.MOVIE)); - assertEquals(BackendType.NZEDB, checkCapsRespone.getIndexerConfig().getBackend()); + assertThat(checkCapsRespone.getIndexerConfig().getBackend()).isEqualTo(BackendType.NZEDB); assertTrue(checkCapsRespone.isAllCapsChecked()); @@ -142,17 +143,17 @@ public class NewznabCheckerTest { } @Test - public void shouldIdentifyCategoryMapping() throws Exception { + void shouldIdentifyCategoryMapping() throws Exception { String xml = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/nzbsOrgCapsResponse.xml"), Charsets.UTF_8); capsRoot = (CapsXmlRoot) unmarshaller.unmarshal(new StreamSource(new StringReader(xml))); when(indexerWebAccess.get(any(), eq(indexerConfig))).thenReturn(capsRoot); testee.setSupportedSearchTypesAndIndexerCategoryMapping(indexerConfig, 100); IndexerCategoryConfig categoryConfig = indexerConfig.getCategoryMapping(); - assertThat(categoryConfig.getAnime().isPresent(), is(true)); - assertThat(categoryConfig.getAnime().get(), is(7040)); - assertThat(categoryConfig.getNameFromId(7040), is("Other Anime")); - assertThat(categoryConfig.getNameFromId(5040), is("TV HD")); + assertThat(categoryConfig.getAnime().isPresent()).isEqualTo(true); + assertThat(categoryConfig.getAnime().get()).isEqualTo(7040); + assertThat(categoryConfig.getNameFromId(7040)).isEqualTo("Other Anime"); + assertThat(categoryConfig.getNameFromId(5040)).isEqualTo("TV HD"); //Test with dog which has different mappings xml = Resources.toString(Resources.getResource(BinsearchTest.class, "/org/nzbhydra/mapping/dognzbCapsResponse.xml"), Charsets.UTF_8); @@ -161,59 +162,59 @@ public class NewznabCheckerTest { testee.setSupportedSearchTypesAndIndexerCategoryMapping(indexerConfig, 100); categoryConfig = indexerConfig.getCategoryMapping(); - assertThat(categoryConfig.getAnime().isPresent(), is(true)); - assertThat(categoryConfig.getAnime().get(), is(5070)); - assertThat(categoryConfig.getComic().isPresent(), is(true)); - assertThat(categoryConfig.getComic().get(), is(7030)); - assertThat(categoryConfig.getNameFromId(7040), is("N/A")); - assertThat(categoryConfig.getNameFromId(5040), is("TV HD")); + assertThat(categoryConfig.getAnime().isPresent()).isEqualTo(true); + assertThat(categoryConfig.getAnime().get()).isEqualTo(5070); + assertThat(categoryConfig.getComic().isPresent()).isEqualTo(true); + assertThat(categoryConfig.getComic().get()).isEqualTo(7030); + assertThat(categoryConfig.getNameFromId(7040)).isEqualTo("N/A"); + assertThat(categoryConfig.getNameFromId(5040)).isEqualTo("TV HD"); } @Test - public void shouldCheckCapsWithoutSupport() throws Exception { + void shouldCheckCapsWithoutSupport() throws Exception { NewznabResponseBuilder builder = new NewznabResponseBuilder(); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&tvdbid=121361"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&rid=24493"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&tvmazeid=82"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&traktid=1390"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=movie&tmdbid=24428"), indexerConfig)) .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=movie&imdbid=0848228"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "somethingElse", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&imdbid=0944947"), indexerConfig)) .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); CheckCapsResponse checkCapsRespone = testee.checkCaps(indexerConfig); - assertEquals(1, checkCapsRespone.getIndexerConfig().getSupportedSearchIds().size()); + assertThat(checkCapsRespone.getIndexerConfig().getSupportedSearchIds()).hasSize(1); verify(indexerWebAccess, times(8)).get(any(), eq(indexerConfig)); } @Test - public void shouldSaySoIfNotAllWereChecked() throws Exception { + void shouldSaySoIfNotAllWereChecked() throws Exception { NewznabResponseBuilder builder = new NewznabResponseBuilder(); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&tvdbid=121361"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&rid=24493"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&tvmazeid=82"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&traktid=1390"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=tvsearch&imdbid=0944947"), indexerConfig)) .thenReturn(builder.getTestResult(1, 100, "Thrones", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=movie&tmdbid=24428"), indexerConfig)) - .thenReturn(builder.getTestResult(1, 100, "Avengers", 0, 100)); + .thenReturn(builder.getTestResult(1, 100, "Avengers", 0, 100)); when(indexerWebAccess.get(new URI("http://127.0.0.1:1234/api?apikey=apikey&t=movie&imdbid=0848228"), indexerConfig)) - .thenThrow(new IndexerAccessException("some error")); + .thenThrow(new IndexerAccessException("some error")); CheckCapsResponse checkCapsRespone = testee.checkCaps(indexerConfig); - assertEquals(6, checkCapsRespone.getIndexerConfig().getSupportedSearchIds().size()); - assertFalse(checkCapsRespone.isAllCapsChecked()); + assertThat(checkCapsRespone.getIndexerConfig().getSupportedSearchIds()).hasSize(6); + assertThat(checkCapsRespone.isAllCapsChecked()).isFalse(); verify(indexerWebAccess, times(8)).get(any(), eq(indexerConfig)); } diff --git a/core/src/test/java/org/nzbhydra/indexers/torznab/TorznabTest.java b/core/src/test/java/org/nzbhydra/indexers/torznab/TorznabTest.java index 48dd60143..fba680f94 100644 --- a/core/src/test/java/org/nzbhydra/indexers/torznab/TorznabTest.java +++ b/core/src/test/java/org/nzbhydra/indexers/torznab/TorznabTest.java @@ -17,8 +17,8 @@ package org.nzbhydra.indexers.torznab; import com.google.common.collect.Lists; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Captor; @@ -29,10 +29,14 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.SearchSourceRestriction; import org.nzbhydra.config.SearchingConfig; import org.nzbhydra.config.category.Category; +import org.nzbhydra.config.downloading.DownloadType; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.IndexerAccessResult; import org.nzbhydra.indexers.IndexerApiAccessRepository; import org.nzbhydra.indexers.IndexerEntity; @@ -46,13 +50,9 @@ import org.nzbhydra.mapping.newznab.xml.NewznabXmlEnclosure; import org.nzbhydra.mapping.newznab.xml.NewznabXmlGuid; import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; import org.nzbhydra.mediainfo.InfoProvider; -import org.nzbhydra.mediainfo.MediaIdType; import org.nzbhydra.searching.CategoryProvider; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchResultItem.DownloadType; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.springframework.web.util.UriComponentsBuilder; import java.time.Instant; @@ -62,8 +62,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -76,8 +75,8 @@ public class TorznabTest { private InfoProvider infoProviderMock; @Mock private IndexerWebAccess indexerWebAccessMock; - @Mock - private IndexerEntity indexerEntityMock; + + private IndexerEntity indexerEntityMock = new IndexerEntity("indexer"); @Mock private CategoryProvider categoryProviderMock; @Mock @@ -104,10 +103,10 @@ public class TorznabTest { private QueryGenerator queryGeneratorMock; @InjectMocks - private Torznab testee = new Torznab(); + private Torznab testee = new Torznab(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); testee = spy(testee); @@ -137,7 +136,7 @@ public class TorznabTest { @Test - public void shouldCreateSearchResultItem() throws Exception { + void shouldCreateSearchResultItem() throws Exception { NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.setSize(456L); rssItem.getTorznabAttributes().add(new NewznabAttribute("password", "0")); @@ -152,31 +151,31 @@ public class TorznabTest { rssItem.setCategory("4000"); SearchResultItem item = testee.createSearchResultItem(rssItem); - assertThat(item.getLink(), is("http://indexer.com/abc")); - assertThat(item.getIndexerGuid(), is("http://indexer.com/123RssGuid")); - assertThat(item.getSize(), is(456L)); - assertThat(item.getCommentsLink(), is("http://indexer.com/123/details#comments")); - assertThat(item.getDetails(), is("http://indexer.com/123/details#comments")); - assertThat(item.isAgePrecise(), is(true)); - assertThat(item.getGrabs(), is(20)); - assertThat(item.getDownloadType(), is(DownloadType.TORRENT)); + assertThat(item.getLink()).isEqualTo("http://indexer.com/abc"); + assertThat(item.getIndexerGuid()).isEqualTo("http://indexer.com/123RssGuid"); + assertThat(item.getSize()).isEqualTo(456L); + assertThat(item.getCommentsLink()).isEqualTo("http://indexer.com/123/details#comments"); + assertThat(item.getDetails()).isEqualTo("http://indexer.com/123/details#comments"); + assertThat(item.isAgePrecise()).isEqualTo(true); + assertThat(item.getGrabs()).isEqualTo(20); + assertThat(item.getDownloadType()).isEqualTo(DownloadType.TORRENT); } @Test - public void shouldComputeCategory() throws Exception { + void shouldComputeCategory() throws Exception { when(categoryProviderMock.fromResultNewznabCategories(ArgumentMatchers.any())).thenReturn(categoryMock); NewznabXmlItem rssItem = buildBasicRssItem(); rssItem.getTorznabAttributes().add(new NewznabAttribute("category", "5070")); rssItem.getEnclosures().add(new NewznabXmlEnclosure("url", 1L, "application/x-bittorrent")); SearchResultItem item = testee.createSearchResultItem(rssItem); - assertThat(item.getCategory(), is(categoryMock)); + assertThat(item.getCategory()).isEqualTo(categoryMock); rssItem.getTorznabAttributes().clear(); rssItem.setCategory("5070"); item = testee.createSearchResultItem(rssItem); - assertThat(item.getCategory(), is(categoryMock)); + assertThat(item.getCategory()).isEqualTo(categoryMock); } private NewznabXmlItem buildBasicRssItem() { @@ -193,48 +192,48 @@ public class TorznabTest { } @Test - public void shouldNotAddExcludedWordsToQuery() throws Exception { + void shouldNotAddExcludedWordsToQuery() throws Exception { SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); searchRequest.getInternalData().setForbiddenWords(Arrays.asList("notthis", "alsonotthis")); searchRequest.setQuery("query"); UriComponentsBuilder builder = testee.buildSearchUrl(searchRequest, 0, 100); - assertThat(builder.toUriString(), not(containsString("notthis"))); + assertThat(builder.toUriString()).doesNotContain("notthis"); } @Test - public void shouldGetCorrectCategoryNumber() { + void shouldGetCorrectCategoryNumber() { NewznabXmlItem item = buildBasicRssItem(); item.setTorznabAttributes(Collections.singletonList(new NewznabAttribute("category", "2000"))); List integers = testee.tryAndGetCategoryAsNumber(item); - assertThat(integers.size(), is(1)); - assertThat(integers.get(0), is(2000)); + assertThat(integers.size()).isEqualTo(1); + assertThat(integers.get(0)).isEqualTo(2000); item.setTorznabAttributes(Collections.singletonList(new NewznabAttribute("category", "10000"))); integers = testee.tryAndGetCategoryAsNumber(item); - assertThat(integers.size(), is(1)); - assertThat(integers.get(0), is(10000)); + assertThat(integers.size()).isEqualTo(1); + assertThat(integers.get(0)).isEqualTo(10000); item.setTorznabAttributes(Arrays.asList(new NewznabAttribute("category", "2000"), new NewznabAttribute("category", "10000"))); integers = testee.tryAndGetCategoryAsNumber(item); integers.sort(Comparator.naturalOrder()); - assertThat(integers.size(), is(2)); - assertThat(integers.get(0), is(2000)); - assertThat(integers.get(1), is(10000)); + assertThat(integers.size()).isEqualTo(2); + assertThat(integers.get(0)).isEqualTo(2000); + assertThat(integers.get(1)).isEqualTo(10000); item.setTorznabAttributes(Arrays.asList(new NewznabAttribute("category", "2000"), new NewznabAttribute("category", "2040"))); integers = testee.tryAndGetCategoryAsNumber(item); integers.sort(Comparator.naturalOrder()); - assertThat(integers.size(), is(2)); - assertThat(integers.get(0), is(2000)); - assertThat(integers.get(1), is(2040)); + assertThat(integers.size()).isEqualTo(2); + assertThat(integers.get(0)).isEqualTo(2000); + assertThat(integers.get(1)).isEqualTo(2040); item.setTorznabAttributes(Arrays.asList(new NewznabAttribute("category", "2000"), new NewznabAttribute("category", "2040"), new NewznabAttribute("category", "10000"))); integers = testee.tryAndGetCategoryAsNumber(item); integers.sort(Comparator.naturalOrder()); - assertThat(integers.size(), is(3)); - assertThat(integers.get(0), is(2000)); - assertThat(integers.get(1), is(2040)); + assertThat(integers.size()).isEqualTo(3); + assertThat(integers.get(0)).isEqualTo(2000); + assertThat(integers.get(1)).isEqualTo(2040); } diff --git a/core/src/test/java/org/nzbhydra/logging/LogAnonymizerTest.java b/core/src/test/java/org/nzbhydra/logging/LogAnonymizerTest.java index 39c49278e..280e6b9a0 100644 --- a/core/src/test/java/org/nzbhydra/logging/LogAnonymizerTest.java +++ b/core/src/test/java/org/nzbhydra/logging/LogAnonymizerTest.java @@ -1,8 +1,8 @@ package org.nzbhydra.logging; import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -11,12 +11,11 @@ import org.nzbhydra.config.ConfigProvider; import org.nzbhydra.config.NotificationConfigEntry; import org.nzbhydra.config.auth.UserAuthConfig; import org.nzbhydra.config.indexer.IndexerConfig; -import org.nzbhydra.notifications.NotificationEventType; +import org.nzbhydra.config.notification.NotificationEventType; import java.util.Arrays; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; public class LogAnonymizerTest { @@ -29,7 +28,7 @@ public class LogAnonymizerTest { @InjectMocks private LogAnonymizer testee = new LogAnonymizer(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); BaseConfig baseConfig = new BaseConfig(); @@ -47,8 +46,10 @@ public class LogAnonymizerTest { } @Test - public void shouldAnonymizeIPs() throws Exception { - String toAnonymize = "192.168.0.1 127.0.0.1 2001:db8:3:4:: 64:ff9b:: 2001:db8:a0b:12f0::1 2001:0db8:0a0b:12f0:0000:0000:0000:0001"; + void shouldAnonymizeIPs() throws Exception { + //IPv6 anonymisation disabled for performance reasons +// String toAnonymize = "192.168.0.1 127.0.0.1 2001:db8:3:4:: 64:ff9b:: 2001:db8:a0b:12f0::1 2001:0db8:0a0b:12f0:0000:0000:0000:0001"; + String toAnonymize = "192.168.0.1 127.0.0.1"; String anonymized = testee.getAnonymizedLog(toAnonymize); @@ -60,7 +61,7 @@ public class LogAnonymizerTest { } @Test - public void shouldAnonymizeHost() { + void shouldAnonymizeHost() { String toAnonymize = "2020-10-18 13:48:44.989 DEBUG --- [http-nio--5] o.n.notifications.NotificationHandler : [ID: 08975, Host: 101-202-303-404.a.b.c.net] Some text"; String anonymized = testee.getAnonymizedLog(toAnonymize); @@ -69,7 +70,7 @@ public class LogAnonymizerTest { } @Test - public void shouldAnonymizeAppriseUrls() { + void shouldAnonymizeAppriseUrls() { String toAnonymize = "Sending message to http://127.0.0.1 and http://www.discord.app using http://apprise.url"; String anonymized = testee.getAnonymizedLog(toAnonymize); @@ -78,33 +79,34 @@ public class LogAnonymizerTest { } @Test - public void shouldAnonymizeUsernameFromUrl() throws Exception { + void shouldAnonymizeUsernameFromUrl() throws Exception { String anonymized = testee.getAnonymizedLog("http://arthur:miller@www.domain.com"); - assertThat(anonymized, is("http://:@www.domain.com")); + assertThat(anonymized).isEqualTo("http://:@www.domain.com"); } @Test - public void shouldAnonymizeUsernameFromConfig() throws Exception { + void shouldAnonymizeUsernameFromConfig() throws Exception { String anonymized = testee.getAnonymizedLog("user=someusername USER:someusername username=someusername username:someusername"); - assertThat(anonymized, is("user= USER: username= username:")); + assertThat(anonymized).isEqualTo("user= USER: username= username:"); } @Test - public void shouldAnonymizeApikeysFromConfig() throws Exception { + void shouldAnonymizeApikeysFromConfig() throws Exception { String anonymized = testee.getAnonymizedLog("r=apikey"); - assertThat(anonymized, is("r=")); + assertThat(anonymized).isEqualTo("r="); } @Test - public void shouldAnonymizeCookiesFromConfig() throws Exception { + void shouldAnonymizeCookiesFromConfig() throws Exception { String anonymized = testee.getAnonymizedLog("Cookies: Parsing b[]: remember-me=MTAI4MjHY0MjcXxMTpjM2U0Zjk3OWQwMjk0; Auth-Type=http; Auth-Token=C8wSA1AXvpFVjXCRGKryWtIIZS2TRqf69aZb; HYDRA-XSRF-TOKEN=1a0f551f-2178-4ad7-a0b5-3af8f77675e2"); - assertThat(anonymized, is("Cookies: Parsing b[]: remember-me=0: Auth-Type=http; Auth-Token=b: HYDRA-XSRF-TOKEN=2:")); + assertThat(anonymized).isEqualTo("Cookies: Parsing b[]: remember-me=0: Auth-Type=http; Auth-Token=b: HYDRA-XSRF-TOKEN=2:"); } -} \ No newline at end of file + +} diff --git a/core/src/test/java/org/nzbhydra/logging/SensitiveDataRemovingPatternLayoutEncoderTest.java b/core/src/test/java/org/nzbhydra/logging/SensitiveDataRemovingPatternLayoutEncoderTest.java index 3c787865a..4003d1902 100644 --- a/core/src/test/java/org/nzbhydra/logging/SensitiveDataRemovingPatternLayoutEncoderTest.java +++ b/core/src/test/java/org/nzbhydra/logging/SensitiveDataRemovingPatternLayoutEncoderTest.java @@ -16,14 +16,14 @@ package org.nzbhydra.logging; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class SensitiveDataRemovingPatternLayoutEncoderTest { @Test - public void shouldRemoveSensitiveData() { + void shouldRemoveSensitiveData() { SensitiveDataRemovingPatternLayoutEncoder encoder = new SensitiveDataRemovingPatternLayoutEncoder(); String result = encoder.removeSensitiveData("https://www.indexer.com/api?apikey=12345678&raw=0&t=search&q=abc"); assertThat(result).isEqualTo("https://www.indexer.com/api?apikey=&raw=0&t=search&q=abc"); diff --git a/core/src/test/java/org/nzbhydra/mapping/JackettCapsMappingTest.java b/core/src/test/java/org/nzbhydra/mapping/JackettCapsCustomQueryAndTitleMappingTest.java similarity index 79% rename from core/src/test/java/org/nzbhydra/mapping/JackettCapsMappingTest.java rename to core/src/test/java/org/nzbhydra/mapping/JackettCapsCustomQueryAndTitleMappingTest.java index 5fb2c15cd..1fd4a6556 100644 --- a/core/src/test/java/org/nzbhydra/mapping/JackettCapsMappingTest.java +++ b/core/src/test/java/org/nzbhydra/mapping/JackettCapsCustomQueryAndTitleMappingTest.java @@ -2,11 +2,11 @@ package org.nzbhydra.mapping; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.nzbhydra.mapping.newznab.xml.caps.jackett.JacketCapsXmlRoot; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; @@ -17,13 +17,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; -@RunWith(SpringRunner.class) -public class JackettCapsMappingTest { +@ExtendWith(SpringExtension.class) +public class JackettCapsCustomQueryAndTitleMappingTest { - @Test //@Ignore //Needs tbe mapped from XML to class - public void testMappingFromXml() throws Exception { + @Test + void testMappingFromXml() throws Exception { JacketCapsXmlRoot root = getRssRootFromXml("jackettConfiguredIndexers.xml"); assertThat(root.getIndexers()).hasSize(13); assertThat(root.getIndexers().get(0).getTitle()).isEqualTo("BroadcastTheNet"); @@ -35,11 +35,11 @@ public class JackettCapsMappingTest { private JacketCapsXmlRoot getRssRootFromXml(String xmlFileName) throws IOException { RestTemplate restTemplate = new RestTemplate(); MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); - final URL resource = Resources.getResource(JackettCapsMappingTest.class, xmlFileName); + final URL resource = Resources.getResource(JackettCapsCustomQueryAndTitleMappingTest.class, xmlFileName); final String xmlContent = Resources.toString(resource, Charsets.UTF_8); mockServer.expect(requestTo("/api")).andRespond(withSuccess(xmlContent, MediaType.APPLICATION_XML)); return restTemplate.getForObject("/api", JacketCapsXmlRoot.class); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/mapping/RssCapsMappingTest.java b/core/src/test/java/org/nzbhydra/mapping/RssCapsCustomQueryAndTitleMappingTest.java similarity index 68% rename from core/src/test/java/org/nzbhydra/mapping/RssCapsMappingTest.java rename to core/src/test/java/org/nzbhydra/mapping/RssCapsCustomQueryAndTitleMappingTest.java index b87838b4b..18f1af869 100644 --- a/core/src/test/java/org/nzbhydra/mapping/RssCapsMappingTest.java +++ b/core/src/test/java/org/nzbhydra/mapping/RssCapsCustomQueryAndTitleMappingTest.java @@ -3,44 +3,43 @@ package org.nzbhydra.mapping; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.xml.bind.JAXBException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlCategory; import org.nzbhydra.mapping.newznab.xml.caps.CapsXmlRoot; import org.nzbhydra.web.WebConfiguration; import org.springframework.http.MediaType; import org.springframework.oxm.jaxb.Jaxb2Marshaller; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; -import javax.xml.bind.JAXBException; import javax.xml.transform.stream.StreamResult; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; -@RunWith(SpringRunner.class) -public class RssCapsMappingTest { +@ExtendWith(SpringExtension.class) +public class RssCapsCustomQueryAndTitleMappingTest { private Jaxb2Marshaller jaxb2Marshaller = new WebConfiguration().marshaller(); - @Before + @BeforeEach public void setUp() throws Exception { } @Test - public void shouldGenerateCorrectXml() throws JsonProcessingException, JAXBException { + void shouldGenerateCorrectXml() throws JsonProcessingException, JAXBException { CapsXmlRoot caps = new CapsXmlRoot(); List categories = new ArrayList<>(); CapsXmlCategory capsCategory = new CapsXmlCategory(1000, "1000"); @@ -52,21 +51,21 @@ public class RssCapsMappingTest { StreamResult streamResult = new StreamResult(writer); jaxb2Marshaller.marshal(caps, streamResult); - assertThat(writer.toString(), containsString("\n" + - " \n" + - " \n" + - " \n" + - " ")); + assertThat(writer.toString()).contains("\n" + + " \n" + + " \n" + + " \n" + + " "); } private CapsXmlRoot getCaps(String xmlFileName) throws IOException { RestTemplate restTemplate = new RestTemplate(); MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); - mockServer.expect(requestTo("/api")).andRespond(withSuccess(Resources.toString(Resources.getResource(RssCapsMappingTest.class, xmlFileName), Charsets.UTF_8), MediaType.APPLICATION_XML)); + mockServer.expect(requestTo("/api")).andRespond(withSuccess(Resources.toString(Resources.getResource(RssCapsCustomQueryAndTitleMappingTest.class, xmlFileName), Charsets.UTF_8), MediaType.APPLICATION_XML)); return restTemplate.getForObject("/api", CapsXmlRoot.class); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/mapping/RssCustomQueryAndTitleMappingTest.java b/core/src/test/java/org/nzbhydra/mapping/RssCustomQueryAndTitleMappingTest.java new file mode 100644 index 000000000..350e93ac2 --- /dev/null +++ b/core/src/test/java/org/nzbhydra/mapping/RssCustomQueryAndTitleMappingTest.java @@ -0,0 +1,235 @@ +package org.nzbhydra.mapping; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.nzbhydra.mapping.newznab.xml.NewznabAttribute; +import org.nzbhydra.mapping.newznab.xml.NewznabXmlChannel; +import org.nzbhydra.mapping.newznab.xml.NewznabXmlEnclosure; +import org.nzbhydra.mapping.newznab.xml.NewznabXmlGuid; +import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; +import org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse; +import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +@ExtendWith(SpringExtension.class) +public class RssCustomQueryAndTitleMappingTest { + @BeforeEach + public void setUp() throws Exception { + + } + + @Test + void testMappingFromXml() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("newznab_3results.xml"); + NewznabXmlChannel channel = rssRoot.getRssChannel(); + assertThat(channel.getDescription()).isEqualTo("indexerName(dot)com Feed"); + assertThat(channel.getLink()).isEqualTo("https://indexerName.com/"); + assertThat(channel.getLanguage()).isEqualTo("en-gb"); + assertThat(channel.getWebMaster()).isEqualTo("admin@indexerName.com (indexerName(dot)com)"); + + NewznabXmlResponse newznabResponse = channel.getNewznabResponse(); + assertThat(newznabResponse.getOffset()).isEqualTo(0); + assertThat(newznabResponse.getTotal()).isEqualTo(1000); + + List items = channel.getItems(); + assertThat(items.size()).isEqualTo(3); + + NewznabXmlItem item = items.get(0); + assertThat(item.getLink()).isEqualTo("https://indexerName.com/getnzb/eff551fbdb69d6777d5030c209ee5d4b.nzb&i=1692&r=apikey"); + assertThat(item.getPubDate()).isEqualTo(Instant.ofEpochSecond(1444584857)); + assertThat(item.getDescription()).isEqualTo("testtitle1"); + assertThat(item.getComments()).isEqualTo("https://indexerName.com/details/eff551fbdb69d6777d5030c209ee5d4b#comments"); + + NewznabXmlGuid rssGuid = item.getRssGuid(); + assertThat(rssGuid.getGuid()).isEqualTo("eff551fbdb69d6777d5030c209ee5d4b"); + assertThat(rssGuid.isPermaLink()).isEqualTo(false); + + NewznabXmlEnclosure enclosure = item.getEnclosure(); + assertThat(enclosure.getUrl()).isEqualTo("https://indexerName.com/getnzb/eff551fbdb69d6777d5030c209ee5d4b.nzb&i=1692&r=apikey"); + assertThat(enclosure.getLength()).isEqualTo(2893890900L); + + List attributes = item.getNewznabAttributes(); + assertThat(attributes.size()).isEqualTo(6); + assertThat(attributes.get(0).getName()).isEqualTo("category"); + assertThat(attributes.get(0).getValue()).isEqualTo("7000"); + assertThat(attributes.get(2).getName()).isEqualTo("size"); + assertThat(attributes.get(2).getValue()).isEqualTo("2893890900"); + assertThat(attributes.get(3).getName()).isEqualTo("guid"); + assertThat(attributes.get(3).getValue()).isEqualTo("eff551fbdb69d6777d5030c209ee5d4b"); + assertThat(attributes.get(4).getName()).isEqualTo("poster"); + assertThat(attributes.get(4).getValue()).isEqualTo("chuck@norris.com"); + assertThat(attributes.get(5).getName()).isEqualTo("group"); + assertThat(attributes.get(5).getValue()).isEqualTo("alt.binaries.mom"); + } + + @Test + void shouldParseResponseFromNzbsOrg() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("nzbsOrgResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(1000)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromOmgwtf() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("omgwtfResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(416)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromNzbAG() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("nzbAgResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(125000)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromTabulaRasa() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("tabulaRasaResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(125000)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromNzbCat() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("nzbCatResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(125000)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromDrunkenSlug() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("drunkenSlugResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(125000)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromDrunkenSlugWithSomeApilimits() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("drunkenSlug_withSomeLimits.xml"); + + assertThat(rssRoot.getRssChannel().getApiLimits().getApiCurrent()).isEqualTo(Integer.valueOf(44)); + assertThat(rssRoot.getRssChannel().getApiLimits().getApiMax()).isEqualTo(Integer.valueOf(50)); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabCurrent()).isNull(); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabMax()).isEqualTo(Integer.valueOf(5)); + } + + @Test + void shouldParseResponseFromDrunkenSlugWithApilimits() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("drunkenSlug_withLimits.xml"); + + assertThat(rssRoot.getRssChannel().getApiLimits().getApiCurrent()).isEqualTo(Integer.valueOf(44)); + assertThat(rssRoot.getRssChannel().getApiLimits().getApiMax()).isEqualTo(Integer.valueOf(50)); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabCurrent()).isEqualTo(Integer.valueOf(1)); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabMax()).isEqualTo(Integer.valueOf(5)); + assertThat(rssRoot.getRssChannel().getApiLimits().getApiOldestTime()).isNull(); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabOldestTime()).isNull(); + } + + @Test + void shouldParseResponseFromTabulaRasaWithApilimits() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("tabluaRasa_withSomeLimits.xml"); + + assertThat(rssRoot.getRssChannel().getApiLimits().getApiCurrent()).isEqualTo(Integer.valueOf(763)); + assertThat(rssRoot.getRssChannel().getApiLimits().getApiMax()).isEqualTo(Integer.valueOf(8000)); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabCurrent()).isEqualTo(Integer.valueOf(4)); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabMax()).isEqualTo(Integer.valueOf(8001)); + assertThat(rssRoot.getRssChannel().getApiLimits().getApiOldestTime().toString()).isEqualTo("2020-04-30T10:39:38Z"); + assertThat(rssRoot.getRssChannel().getApiLimits().getGrabOldestTime().toString()).isEqualTo("2020-04-30T17:54:30Z"); + } + + @Test + void shouldParseResponseFromNzbFinder() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("nzbFinderResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(125000)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromNewztown() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("newztownResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(4443964)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromNzbSu() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("nzbSuResponse.xml"); + + assertThat(rssRoot.getRssChannel().getNewznabResponse().getTotal()).isEqualTo(Integer.valueOf(20000)); + assertThat(rssRoot.getRssChannel().getItems().get(0).getPubDate()).isNotNull(); + } + + @Test + void shouldParseResponseFromCardigann() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("btnJackettResponse.xml"); + + NewznabXmlChannel channel = rssRoot.getRssChannel(); + assertThat(channel.getDescription()).isEqualTo("Needs no description.."); + + NewznabXmlResponse newznabResponse = channel.getNewznabResponse(); + assertThat(newznabResponse).isNull(); + + List items = channel.getItems(); + assertThat(items.size()).isEqualTo(3); + NewznabXmlItem item = items.get(0); + assertThat(item.getTitle()).isEqualTo("The.Challenge.S30.Special.14.Times.Our.Challengers.Found.Their.Shit.1080p.WEB.x264-CookieMonster"); + assertThat(item.getRssGuid().getGuid()).isEqualTo("https://unicasthe.net/torrents.php?action=download&id=799031&authkey=authkey&torrent_pass=torrentPass"); + assertThat(item.getLink()).startsWith("http://127.0.0.1:9117/dl/unicasthenet/jackettApiKey?path=linkstuff&file=The.Challenge.S30.Special.14.Times.Our.Challengers.Found.Their.Shit.1080p.WEB.x264-CookieMonster.torrent"); + assertThat(item.getCategory()).isEqualTo("5000"); + assertThat(item.getEnclosure().getLength()).isEqualTo(1459519537L); + assertThat(item.getTorznabAttributes().size()).isEqualTo(6); + assertThat(item.getTorznabAttributes().get(0).getName()).isEqualTo("rageid"); + assertThat(item.getTorznabAttributes().get(0).getValue()).isEqualTo("6126"); + } + + @Test + void shouldParseResponseFromNzbIndex() throws Exception { + NewznabXmlRoot rssRoot = getRssRootFromXml("nzbIndexResponse.xml"); + assertThat(rssRoot.getRssChannel().getItems()).hasSize(100); + } + + + private NewznabXmlRoot getRssRootFromXml(String xmlFileName) throws IOException { + RestTemplate restTemplate = new RestTemplate(); + MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); + mockServer.expect(requestTo("/api")).andRespond(withSuccess(Resources.toString(Resources.getResource(RssCustomQueryAndTitleMappingTest.class, xmlFileName), Charsets.UTF_8), MediaType.APPLICATION_XML)); + + return restTemplate.getForObject("/api", NewznabXmlRoot.class); + } + + @Test + void shouldSerializeToJson() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + NewznabXmlRoot rssRoot = getRssRootFromXml("newznab_3results.xml"); + String json = objectMapper.writeValueAsString(rssRoot); + System.out.println(json); + } + + +} diff --git a/core/src/test/java/org/nzbhydra/mapping/RssMappingTest.java b/core/src/test/java/org/nzbhydra/mapping/RssMappingTest.java deleted file mode 100644 index 39bf755ea..000000000 --- a/core/src/test/java/org/nzbhydra/mapping/RssMappingTest.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.nzbhydra.mapping; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.nzbhydra.mapping.newznab.xml.NewznabAttribute; -import org.nzbhydra.mapping.newznab.xml.NewznabXmlChannel; -import org.nzbhydra.mapping.newznab.xml.NewznabXmlEnclosure; -import org.nzbhydra.mapping.newznab.xml.NewznabXmlGuid; -import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; -import org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse; -import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; -import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.client.MockRestServiceServer; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.time.Instant; -import java.util.List; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - -@RunWith(SpringRunner.class) -public class RssMappingTest { - @Before - public void setUp() throws Exception { - - } - - @Test - public void testMappingFromXml() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("newznab_3results.xml"); - NewznabXmlChannel channel = rssRoot.getRssChannel(); - assertThat(channel.getDescription(), is("indexerName(dot)com Feed")); - assertThat(channel.getLink(), is("https://indexerName.com/")); - assertThat(channel.getLanguage(), is("en-gb")); - assertThat(channel.getWebMaster(), is("admin@indexerName.com (indexerName(dot)com)")); - - NewznabXmlResponse newznabResponse = channel.getNewznabResponse(); - assertThat(newznabResponse.getOffset(), is(0)); - assertThat(newznabResponse.getTotal(), is(1000)); - - List items = channel.getItems(); - assertThat(items.size(), is(3)); - - NewznabXmlItem item = items.get(0); - assertThat(item.getLink(), is("https://indexerName.com/getnzb/eff551fbdb69d6777d5030c209ee5d4b.nzb&i=1692&r=apikey")); - assertThat(item.getPubDate(), is(Instant.ofEpochSecond(1444584857))); - assertThat(item.getDescription(), is("testtitle1")); - assertThat(item.getComments(), is("https://indexerName.com/details/eff551fbdb69d6777d5030c209ee5d4b#comments")); - - NewznabXmlGuid rssGuid = item.getRssGuid(); - assertThat(rssGuid.getGuid(), is("eff551fbdb69d6777d5030c209ee5d4b")); - assertThat(rssGuid.isPermaLink(), is(false)); - - NewznabXmlEnclosure enclosure = item.getEnclosure(); - assertThat(enclosure.getUrl(), is("https://indexerName.com/getnzb/eff551fbdb69d6777d5030c209ee5d4b.nzb&i=1692&r=apikey")); - assertThat(enclosure.getLength(), is(2893890900L)); - - List attributes = item.getNewznabAttributes(); - assertThat(attributes.size(), is(6)); - assertThat(attributes.get(0).getName(), is("category")); - assertThat(attributes.get(0).getValue(), is("7000")); - assertThat(attributes.get(2).getName(), is("size")); - assertThat(attributes.get(2).getValue(), is("2893890900")); - assertThat(attributes.get(3).getName(), is("guid")); - assertThat(attributes.get(3).getValue(), is("eff551fbdb69d6777d5030c209ee5d4b")); - assertThat(attributes.get(4).getName(), is("poster")); - assertThat(attributes.get(4).getValue(), is("chuck@norris.com")); - assertThat(attributes.get(5).getName(), is("group")); - assertThat(attributes.get(5).getValue(), is("alt.binaries.mom")); - } - - @Test - public void shouldParseResponseFromNzbsOrg() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("nzbsOrgResponse.xml"); - - assertEquals(Integer.valueOf(1000), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromOmgwtf() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("omgwtfResponse.xml"); - - assertEquals(Integer.valueOf(416), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromNzbAG() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("nzbAgResponse.xml"); - - assertEquals(Integer.valueOf(125000), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromTabulaRasa() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("tabulaRasaResponse.xml"); - - assertEquals(Integer.valueOf(125000), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromNzbCat() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("nzbCatResponse.xml"); - - assertEquals(Integer.valueOf(125000), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromDrunkenSlug() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("drunkenSlugResponse.xml"); - - assertEquals(Integer.valueOf(125000), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromDrunkenSlugWithSomeApilimits() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("drunkenSlug_withSomeLimits.xml"); - - assertEquals(Integer.valueOf(44), rssRoot.getRssChannel().getApiLimits().getApiCurrent()); - assertEquals(Integer.valueOf(50), rssRoot.getRssChannel().getApiLimits().getApiMax()); - assertNull(rssRoot.getRssChannel().getApiLimits().getGrabCurrent()); - assertEquals(Integer.valueOf(5), rssRoot.getRssChannel().getApiLimits().getGrabMax()); - } - - @Test - public void shouldParseResponseFromDrunkenSlugWithApilimits() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("drunkenSlug_withLimits.xml"); - - assertEquals(Integer.valueOf(44), rssRoot.getRssChannel().getApiLimits().getApiCurrent()); - assertEquals(Integer.valueOf(50), rssRoot.getRssChannel().getApiLimits().getApiMax()); - assertEquals(Integer.valueOf(1), rssRoot.getRssChannel().getApiLimits().getGrabCurrent()); - assertEquals(Integer.valueOf(5), rssRoot.getRssChannel().getApiLimits().getGrabMax()); - assertNull(rssRoot.getRssChannel().getApiLimits().getApiOldestTime()); - assertNull(rssRoot.getRssChannel().getApiLimits().getGrabOldestTime()); - } - - @Test - public void shouldParseResponseFromTabulaRasaWithApilimits() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("tabluaRasa_withSomeLimits.xml"); - - assertEquals(Integer.valueOf(763), rssRoot.getRssChannel().getApiLimits().getApiCurrent()); - assertEquals(Integer.valueOf(8000), rssRoot.getRssChannel().getApiLimits().getApiMax()); - assertEquals(Integer.valueOf(4), rssRoot.getRssChannel().getApiLimits().getGrabCurrent()); - assertEquals(Integer.valueOf(8001), rssRoot.getRssChannel().getApiLimits().getGrabMax()); - assertEquals("2020-04-30T10:39:38Z", rssRoot.getRssChannel().getApiLimits().getApiOldestTime().toString()); - assertEquals("2020-04-30T17:54:30Z", rssRoot.getRssChannel().getApiLimits().getGrabOldestTime().toString()); - } - - @Test - public void shouldParseResponseFromNzbFinder() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("nzbFinderResponse.xml"); - - assertEquals(Integer.valueOf(125000), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromNewztown() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("newztownResponse.xml"); - - assertEquals(Integer.valueOf(4443964), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromNzbSu() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("nzbSuResponse.xml"); - - assertEquals(Integer.valueOf(20000), rssRoot.getRssChannel().getNewznabResponse().getTotal()); - assertNotNull(rssRoot.getRssChannel().getItems().get(0).getPubDate()); - } - - @Test - public void shouldParseResponseFromCardigann() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("btnJackettResponse.xml"); - - NewznabXmlChannel channel = rssRoot.getRssChannel(); - assertThat(channel.getDescription(), is("Needs no description..")); - - NewznabXmlResponse newznabResponse = channel.getNewznabResponse(); - assertThat(newznabResponse, is(nullValue())); - - List items = channel.getItems(); - assertThat(items.size(), is(3)); - NewznabXmlItem item = items.get(0); - assertThat(item.getTitle(), is("The.Challenge.S30.Special.14.Times.Our.Challengers.Found.Their.Shit.1080p.WEB.x264-CookieMonster")); - assertThat(item.getRssGuid().getGuid(), is("https://unicasthe.net/torrents.php?action=download&id=799031&authkey=authkey&torrent_pass=torrentPass")); - assertThat(item.getLink(), startsWith("http://127.0.0.1:9117/dl/unicasthenet/jackettApiKey?path=linkstuff&file=The.Challenge.S30.Special.14.Times.Our.Challengers.Found.Their.Shit.1080p.WEB.x264-CookieMonster.torrent")); - assertThat(item.getCategory(), is("5000")); - assertThat(item.getEnclosure().getLength(), is(1459519537L)); - assertThat(item.getTorznabAttributes().size(), is(6)); - assertThat(item.getTorznabAttributes().get(0).getName(), is("rageid")); - assertThat(item.getTorznabAttributes().get(0).getValue(), is("6126")); - } - - @Test - public void shouldParseResponseFromNzbIndex() throws Exception { - NewznabXmlRoot rssRoot = getRssRootFromXml("nzbIndexResponse.xml"); - assertEquals(100, rssRoot.getRssChannel().getItems().size()); - } - - - private NewznabXmlRoot getRssRootFromXml(String xmlFileName) throws IOException { - RestTemplate restTemplate = new RestTemplate(); - MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); - mockServer.expect(requestTo("/api")).andRespond(withSuccess(Resources.toString(Resources.getResource(RssMappingTest.class, xmlFileName), Charsets.UTF_8), MediaType.APPLICATION_XML)); - - return restTemplate.getForObject("/api", NewznabXmlRoot.class); - } - - @Test - public void shouldSerializeToJson() throws Exception{ - ObjectMapper objectMapper = new ObjectMapper(); - NewznabXmlRoot rssRoot = getRssRootFromXml("newznab_3results.xml"); - String json = objectMapper.writeValueAsString(rssRoot); - System.out.println(json); - } - - -} diff --git a/core/src/test/java/org/nzbhydra/mapping/TorznabMappingTest.java b/core/src/test/java/org/nzbhydra/mapping/TorznabCustomQueryAndTitleMappingTest.java similarity index 52% rename from core/src/test/java/org/nzbhydra/mapping/TorznabMappingTest.java rename to core/src/test/java/org/nzbhydra/mapping/TorznabCustomQueryAndTitleMappingTest.java index 556714d8a..aaa74e958 100644 --- a/core/src/test/java/org/nzbhydra/mapping/TorznabMappingTest.java +++ b/core/src/test/java/org/nzbhydra/mapping/TorznabCustomQueryAndTitleMappingTest.java @@ -2,9 +2,9 @@ package org.nzbhydra.mapping; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.nzbhydra.mapping.newznab.xml.NewznabAttribute; import org.nzbhydra.mapping.newznab.xml.NewznabXmlChannel; import org.nzbhydra.mapping.newznab.xml.NewznabXmlEnclosure; @@ -12,7 +12,7 @@ import org.nzbhydra.mapping.newznab.xml.NewznabXmlGuid; import org.nzbhydra.mapping.newznab.xml.NewznabXmlItem; import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; @@ -20,56 +20,55 @@ import java.io.IOException; import java.time.Instant; import java.util.List; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; -@RunWith(SpringRunner.class) -public class TorznabMappingTest { +@ExtendWith(SpringExtension.class) +public class TorznabCustomQueryAndTitleMappingTest { - @Before + @BeforeEach public void setUp() throws Exception { } @Test - public void testMappingFromXml() throws Exception { + void testMappingFromXml() throws Exception { NewznabXmlRoot rssRoot = getRssRootFromXml("cardigann.xml"); NewznabXmlChannel channel = rssRoot.getRssChannel(); - assertThat(channel.getTitle(), is("some-torrents")); - assertThat(channel.getLink(), is("https://some-torrents.com/")); - assertThat(channel.getLanguage(), is("en-us")); + assertThat(channel.getTitle()).isEqualTo("some-torrents"); + assertThat(channel.getLink()).isEqualTo("https://some-torrents.com/"); + assertThat(channel.getLanguage()).isEqualTo("en-us"); List items = channel.getItems(); - assertThat(items.size(), is(2)); + assertThat(items.size()).isEqualTo(2); NewznabXmlItem item = items.get(0); - assertThat(item.getLink(), is("http://127.0.0.1:5060/download/111.torrent")); - assertThat(item.getPubDate(), is(Instant.ofEpochSecond(1493900064))); - assertThat(item.getComments(), is("https://some-torrents.com/details.php?id=111&page=0#startcomments")); + assertThat(item.getLink()).isEqualTo("http://127.0.0.1:5060/download/111.torrent"); + assertThat(item.getPubDate()).isEqualTo(Instant.ofEpochSecond(1493900064)); + assertThat(item.getComments()).isEqualTo("https://some-torrents.com/details.php?id=111&page=0#startcomments"); NewznabXmlGuid rssGuid = item.getRssGuid(); - assertThat(rssGuid.getGuid(), is("https://some-torrents.com/details.php?id=111")); + assertThat(rssGuid.getGuid()).isEqualTo("https://some-torrents.com/details.php?id=111"); NewznabXmlEnclosure enclosure = item.getEnclosure(); - assertThat(enclosure.getUrl(), is("http://127.0.0.1:5060/download/111.torrent")); + assertThat(enclosure.getUrl()).isEqualTo("http://127.0.0.1:5060/download/111.torrent"); List attributes = item.getTorznabAttributes(); - assertThat(attributes.size(), is(8)); - assertThat(attributes.get(1).getName(), is("seeders")); - assertThat(attributes.get(1).getValue(), is("11")); - assertThat(attributes.get(5).getName(), is("size")); - assertThat(attributes.get(5).getValue(), is("620000000")); + assertThat(attributes.size()).isEqualTo(8); + assertThat(attributes.get(1).getName()).isEqualTo("seeders"); + assertThat(attributes.get(1).getValue()).isEqualTo("11"); + assertThat(attributes.get(5).getName()).isEqualTo("size"); + assertThat(attributes.get(5).getValue()).isEqualTo("620000000"); } private NewznabXmlRoot getRssRootFromXml(String xmlFileName) throws IOException { RestTemplate restTemplate = new RestTemplate(); MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); - mockServer.expect(requestTo("/api")).andRespond(withSuccess(Resources.toString(Resources.getResource(TorznabMappingTest.class, xmlFileName), Charsets.UTF_8), MediaType.APPLICATION_XML)); + mockServer.expect(requestTo("/api")).andRespond(withSuccess(Resources.toString(Resources.getResource(TorznabCustomQueryAndTitleMappingTest.class, xmlFileName), Charsets.UTF_8), MediaType.APPLICATION_XML)); return restTemplate.getForObject("/api", NewznabXmlRoot.class); } diff --git a/core/src/test/java/org/nzbhydra/mapping/json/JsonMappingTest.java b/core/src/test/java/org/nzbhydra/mapping/json/JsonCustomQueryAndTitleMappingTest.java similarity index 73% rename from core/src/test/java/org/nzbhydra/mapping/json/JsonMappingTest.java rename to core/src/test/java/org/nzbhydra/mapping/json/JsonCustomQueryAndTitleMappingTest.java index ea57e62c2..fa2a1b668 100644 --- a/core/src/test/java/org/nzbhydra/mapping/json/JsonMappingTest.java +++ b/core/src/test/java/org/nzbhydra/mapping/json/JsonCustomQueryAndTitleMappingTest.java @@ -19,25 +19,22 @@ package org.nzbhydra.mapping.json; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.nzbhydra.mapping.newznab.json.NewznabJsonRoot; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -public class JsonMappingTest { - - +import org.springframework.test.context.junit.jupiter.SpringExtension; +@ExtendWith(SpringExtension.class) +public class JsonCustomQueryAndTitleMappingTest { @Test - public void shouldSerializeToJson() throws Exception{ + void shouldSerializeToJson() throws Exception { ObjectMapper objectMapper = new ObjectMapper(); - String json = Resources.toString(Resources.getResource(JsonMappingTest.class, ("nzbsorg_3items.json").toLowerCase()), Charsets.UTF_8); + String json = Resources.toString(Resources.getResource(JsonCustomQueryAndTitleMappingTest.class, ("nzbsorg_3items.json").toLowerCase()), Charsets.UTF_8); NewznabJsonRoot root = objectMapper.readValue(json, NewznabJsonRoot.class); System.out.println(json); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/mediainfo/CustomTmdbTest.java b/core/src/test/java/org/nzbhydra/mediainfo/CustomTmdbTest.java deleted file mode 100644 index e68e24d39..000000000 --- a/core/src/test/java/org/nzbhydra/mediainfo/CustomTmdbTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * (C) Copyright 2017 TheOtherP (theotherp@posteo.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nzbhydra.mediainfo; - -import com.uwetrottmann.tmdb2.Tmdb; -import com.uwetrottmann.tmdb2.entities.FindResults; -import com.uwetrottmann.tmdb2.enumerations.ExternalSource; -import org.junit.Ignore; -import org.junit.Test; -import org.mockito.InjectMocks; -import retrofit2.Call; - -@Ignore -public class CustomTmdbTest { - - @InjectMocks - private CustomTmdb testee = new CustomTmdb(); - - @Test - public void bla() { - Tmdb tm = new Tmdb("4df99d58875c2d01fc04936759fea56f"); - Call findResultsCall = tm.findService().find("tt0019798", ExternalSource.IMDB_ID, null); - System.out.println(findResultsCall); - } - -} \ No newline at end of file diff --git a/core/src/test/java/org/nzbhydra/mediainfo/InfoProviderTest.java b/core/src/test/java/org/nzbhydra/mediainfo/InfoProviderTest.java index c189d9aea..4528e01de 100644 --- a/core/src/test/java/org/nzbhydra/mediainfo/InfoProviderTest.java +++ b/core/src/test/java/org/nzbhydra/mediainfo/InfoProviderTest.java @@ -1,19 +1,21 @@ package org.nzbhydra.mediainfo; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.internal.util.collections.Sets; +import org.nzbhydra.config.mediainfo.MediaIdType; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.*; public class InfoProviderTest { @@ -30,7 +32,7 @@ public class InfoProviderTest { @InjectMocks private InfoProvider testee = new InfoProvider(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -51,7 +53,7 @@ public class InfoProviderTest { } @Test - public void canConvert() throws Exception { + void canConvert() throws Exception { for (MediaIdType type : Arrays.asList(MediaIdType.IMDB, MediaIdType.TMDB, MediaIdType.MOVIETITLE)) { for (MediaIdType type2 : Arrays.asList(MediaIdType.IMDB, MediaIdType.TMDB, MediaIdType.MOVIETITLE)) { assertTrue(testee.canConvert(type, type2)); @@ -60,36 +62,36 @@ public class InfoProviderTest { for (MediaIdType type : Arrays.asList(MediaIdType.TVMAZE, MediaIdType.TVDB, MediaIdType.TVRAGE, MediaIdType.TVTITLE, MediaIdType.TVIMDB)) { for (MediaIdType type2 : Arrays.asList(MediaIdType.TVMAZE, MediaIdType.TVDB, MediaIdType.TVRAGE, MediaIdType.TVTITLE, MediaIdType.TVIMDB)) { - assertTrue("Should be able to convert " + type + " to " + type2, testee.canConvert(type, type2)); + assertTrue(testee.canConvert(type, type2), "Should be able to convert " + type + " to " + type2); } } } @Test - public void canConvertAny() throws Exception { + void canConvertAny() throws Exception { assertTrue(testee.canConvertAny(Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB), Sets.newSet(MediaIdType.TVRAGE))); assertTrue(testee.canConvertAny(Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB), Sets.newSet(MediaIdType.TVMAZE))); assertTrue(testee.canConvertAny(Sets.newSet(MediaIdType.TVMAZE), Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB))); - assertFalse(testee.canConvertAny(Sets.newSet(), Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB))); - assertFalse(testee.canConvertAny(Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB), Sets.newSet())); + assertThat(testee.canConvertAny(Sets.newSet(), Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB))).isFalse(); + assertThat(testee.canConvertAny(Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB), Sets.newSet())).isFalse(); - assertFalse(testee.canConvertAny(Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB), Sets.newSet(MediaIdType.TMDB))); + assertThat(testee.canConvertAny(Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB), Sets.newSet(MediaIdType.TMDB))).isFalse(); } @Test - public void shouldCatchUnexpectedError() throws Exception { + void shouldCatchUnexpectedError() throws Exception { when(tvMazeHandlerMock.getInfos(anyString(), eq(MediaIdType.TVDB))).thenThrow(IllegalArgumentException.class); try { testee.convert("", MediaIdType.TVDB); fail("Should've failed"); } catch (Exception e) { - assertEquals(InfoProviderException.class, e.getClass()); + assertThat(e.getClass()).isEqualTo(InfoProviderException.class); } } @Test - public void shouldCallTvMaze() throws Exception { + void shouldCallTvMaze() throws Exception { ArgumentCaptor tvInfoArgumentCaptor = ArgumentCaptor.forClass(TvInfo.class); for (MediaIdType type : Arrays.asList(MediaIdType.TVMAZE, MediaIdType.TVDB, MediaIdType.TVRAGE, MediaIdType.TVTITLE, MediaIdType.TVIMDB)) { reset(tvMazeHandlerMock); @@ -103,17 +105,17 @@ public class InfoProviderTest { verify(tvInfoRepositoryMock).findByTvmazeId("value"); verify(tvInfoRepositoryMock).findByImdbId("ttvalue"); verify(tvInfoRepositoryMock, times(5)).save(tvInfoArgumentCaptor.capture()); - assertEquals(5, tvInfoArgumentCaptor.getAllValues().size()); - assertEquals("title", tvInfoArgumentCaptor.getValue().getTitle()); - assertEquals("tvdbId", tvInfoArgumentCaptor.getValue().getTvdbId().get()); - assertEquals("tvmazeId", tvInfoArgumentCaptor.getValue().getTvmazeId().get()); - assertEquals("tvrageId", tvInfoArgumentCaptor.getValue().getTvrageId().get()); - assertEquals("ttimdbId", tvInfoArgumentCaptor.getValue().getImdbId().get()); - assertEquals(Integer.valueOf(0), tvInfoArgumentCaptor.getValue().getYear()); + assertThat(tvInfoArgumentCaptor.getAllValues()).hasSize(5); + assertThat(tvInfoArgumentCaptor.getValue().getTitle()).isEqualTo("title"); + assertThat(tvInfoArgumentCaptor.getValue().getTvdbId().get()).isEqualTo("tvdbId"); + assertThat(tvInfoArgumentCaptor.getValue().getTvmazeId().get()).isEqualTo("tvmazeId"); + assertThat(tvInfoArgumentCaptor.getValue().getTvrageId().get()).isEqualTo("tvrageId"); + assertThat(tvInfoArgumentCaptor.getValue().getImdbId().get()).isEqualTo("ttimdbId"); + assertThat(tvInfoArgumentCaptor.getValue().getYear()).isEqualTo(Integer.valueOf(0)); } @Test - public void shouldCallTmdb() throws Exception { + void shouldCallTmdb() throws Exception { for (MediaIdType type : Arrays.asList(MediaIdType.IMDB, MediaIdType.TMDB, MediaIdType.MOVIETITLE)) { testConvertByType(type, type == MediaIdType.IMDB ? "ttvalue" : "value"); } @@ -129,7 +131,7 @@ public class InfoProviderTest { } @Test - public void shouldSearch() throws Exception { + void shouldSearch() throws Exception { testee.search("title", MediaIdType.TVTITLE); verify(tvMazeHandlerMock).search("title"); @@ -138,16 +140,16 @@ public class InfoProviderTest { } @Test - public void shouldGetInfoWithMostIds() { + void shouldGetInfoWithMostIds() { TvInfo mostInfo = new TvInfo("abc", "abc", "abc", null, null, null, null); when(tvInfoRepositoryMock.findByTvrageIdOrTvmazeIdOrTvdbIdOrImdbId(anyString(), anyString(), anyString(), anyString())).thenReturn(Arrays.asList( - mostInfo, - new TvInfo("abc", "abc", null, null, null, null, null), - new TvInfo("abc", null, null, null, null, null, null) + mostInfo, + new TvInfo("abc", "abc", null, null, null, null, null), + new TvInfo("abc", null, null, null, null, null, null) )); TvInfo info = testee.findTvInfoInDatabase(new HashMap<>()); - assertEquals(mostInfo, info); + assertThat(info).isEqualTo(mostInfo); } } diff --git a/core/src/test/java/org/nzbhydra/mediainfo/TmdbHandlerTest.java b/core/src/test/java/org/nzbhydra/mediainfo/TmdbHandlerTest.java deleted file mode 100644 index 0a481458f..000000000 --- a/core/src/test/java/org/nzbhydra/mediainfo/TmdbHandlerTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.nzbhydra.mediainfo; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.mockito.MockitoAnnotations; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -@Ignore //Needs internet access -public class TmdbHandlerTest { - - TmdbHandler testee = new TmdbHandler(); - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - testee.tmdb = new CustomTmdb("4df99d58875c2d01fc04936759fea56f"); - } - - @Test - public void imdbToTmdb() throws Exception { - TmdbSearchResult result = testee.getInfos("tt5895028", MediaIdType.IMDB); - assertThat(result.getTmdbId(), is("407806")); - assertThat(result.getTitle(), is("13th")); - } - - @Test - public void tmdbToImdb() throws Exception { - TmdbSearchResult result = testee.getInfos("407806", MediaIdType.TMDB); - assertThat(result.getImdbId(), is("tt5895028")); - assertThat(result.getTitle(), is("13th")); - } - - @Test - public void fromTitle() throws Exception { - TmdbSearchResult result = testee.getInfos("gladiator", MediaIdType.MOVIETITLE); - assertThat(result.getImdbId(), is("tt0172495")); - - result = testee.fromTitle("gladiator", 1992); - assertThat(result.getImdbId(), is("tt0104346")); - } - -} \ No newline at end of file diff --git a/core/src/test/java/org/nzbhydra/news/NewsProviderTest.java b/core/src/test/java/org/nzbhydra/news/NewsProviderTest.java index a7378e88d..0bda07a75 100644 --- a/core/src/test/java/org/nzbhydra/news/NewsProviderTest.java +++ b/core/src/test/java/org/nzbhydra/news/NewsProviderTest.java @@ -2,8 +2,8 @@ package org.nzbhydra.news; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -18,8 +18,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -35,7 +34,7 @@ public class NewsProviderTest { @InjectMocks private NewsProvider testee = new NewsProvider(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(webAccessMock.callUrl(any(), any(TypeReference.class))).thenReturn(getNewsJson()); @@ -43,46 +42,46 @@ public class NewsProviderTest { } @Test - public void getNews() throws Exception { + void getNews() throws Exception { List entries = testee.getNews(); - assertThat(entries.size(), is(3)); - assertThat(entries.get(0).getNewsAsMarkdown(), is("news3.0.0")); - assertThat(entries.get(0).getShowForVersion().major, is(3)); + assertThat(entries.size()).isEqualTo(3); + assertThat(entries.get(0).getNewsAsMarkdown()).isEqualTo("news3.0.0"); + assertThat(entries.get(0).getShowForVersion().major).isEqualTo(3); } @Test - public void shouldOnlyGetNewsNewerThanShownButNotNewerThanCurrentVersion() throws Exception { + void shouldOnlyGetNewsNewerThanShownButNotNewerThanCurrentVersion() throws Exception { when(updateManagerMock.getCurrentVersionString()).thenReturn("2.0.0"); when(shownNewsRepositoryMock.findAll()).thenReturn(Collections.singletonList(new ShownNews("1.0.0"))); List entries = testee.getNewsForCurrentVersionAndAfter(); - assertThat(entries.size(), is(1)); - assertThat(entries.get(0).getNewsAsMarkdown(), is("news2.0.0")); + assertThat(entries.size()).isEqualTo(1); + assertThat(entries.get(0).getNewsAsMarkdown()).isEqualTo("news2.0.0"); } @Test - public void shouldOnlyGetNewstNotNewerThanCurrentVersion() throws Exception { + void shouldOnlyGetNewstNotNewerThanCurrentVersion() throws Exception { when(updateManagerMock.getCurrentVersionString()).thenReturn("2.0.0"); when(shownNewsRepositoryMock.findAll()).thenReturn(Collections.singletonList(new ShownNews("0.0.1"))); List entries = testee.getNewsForCurrentVersionAndAfter(); - assertThat(entries.size(), is(2)); - assertThat(entries.get(0).getNewsAsMarkdown(), is("news2.0.0")); - assertThat(entries.get(1).getNewsAsMarkdown(), is("news1.0.0")); + assertThat(entries.size()).isEqualTo(2); + assertThat(entries.get(0).getNewsAsMarkdown()).isEqualTo("news2.0.0"); + assertThat(entries.get(1).getNewsAsMarkdown()).isEqualTo("news1.0.0"); } @Test - public void shouldNoNewsWhenNewInstall() throws Exception { + void shouldNoNewsWhenNewInstall() throws Exception { when(updateManagerMock.getCurrentVersionString()).thenReturn("2.0.0"); when(shownNewsRepositoryMock.findAll()).thenReturn(Collections.emptyList()); List entries = testee.getNewsForCurrentVersionAndAfter(); - assertThat(entries.size(), is(0)); + assertThat(entries.size()).isEqualTo(0); } @Test - public void shouldNotGetNewsWhenAlreadyShown() throws Exception { + void shouldNotGetNewsWhenAlreadyShown() throws Exception { when(updateManagerMock.getCurrentVersionString()).thenReturn("3.0.0"); when(shownNewsRepositoryMock.findAll()).thenReturn(Collections.singletonList(new ShownNews("3.0.0"))); List entries = testee.getNewsForCurrentVersionAndAfter(); - assertThat(entries.size(), is(0)); + assertThat(entries.size()).isEqualTo(0); } protected List getNewsJson() throws ParseException, JsonProcessingException { @@ -95,4 +94,4 @@ public class NewsProviderTest { } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/notifications/NotificationEntityTest.java b/core/src/test/java/org/nzbhydra/notifications/NotificationEntityTest.java new file mode 100644 index 000000000..5b4cefaf2 --- /dev/null +++ b/core/src/test/java/org/nzbhydra/notifications/NotificationEntityTest.java @@ -0,0 +1,46 @@ +/* + * (C) Copyright 2023 TheOtherP (theotherp@posteo.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nzbhydra.notifications; + +import org.junit.jupiter.api.Test; +import org.nzbhydra.Jackson; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationEntityTest { + private final NotificationEntity testee = new NotificationEntity(); + + @Test + public void shouldBeConvertibleToTO() throws Exception { + testee.setTime(Instant.now()); + testee.setBody("body"); + testee.setId(1); + testee.setUrls("urls"); + testee.setMessageType(NotificationMessageType.INFO); + testee.setDisplayed(true); + testee.setTitle("title"); + + + final NotificationEntityTO to = Jackson.JSON_MAPPER.convertValue(testee, NotificationEntityTO.class); + final String jsonTO = Jackson.JSON_MAPPER.writeValueAsString(to); + final String jsonEntity = Jackson.JSON_MAPPER.writeValueAsString(testee); + assertThat(jsonTO).isEqualTo(jsonEntity); + } + +} diff --git a/core/src/test/java/org/nzbhydra/problemdetection/OutOfMemoryDetectorTest.java b/core/src/test/java/org/nzbhydra/problemdetection/OutOfMemoryDetectorTest.java index 27ed8522a..ee335dc87 100644 --- a/core/src/test/java/org/nzbhydra/problemdetection/OutOfMemoryDetectorTest.java +++ b/core/src/test/java/org/nzbhydra/problemdetection/OutOfMemoryDetectorTest.java @@ -16,8 +16,8 @@ package org.nzbhydra.problemdetection; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -48,9 +48,9 @@ public class OutOfMemoryDetectorTest { private OutOfMemoryDetector testee = new OutOfMemoryDetector(); - @Ignore //Fails on Pipeline + @Disabled //Fails on Pipeline @Test - public void executeCheck() throws Exception { + void executeCheck() throws Exception { MockitoAnnotations.initMocks(this); final Path tempFile = Files.createTempFile("nzbhydra", ".log"); diff --git a/core/src/test/java/org/nzbhydra/searching/CategoryProviderTest.java b/core/src/test/java/org/nzbhydra/searching/CategoryProviderTest.java index 99577c0de..93488d763 100644 --- a/core/src/test/java/org/nzbhydra/searching/CategoryProviderTest.java +++ b/core/src/test/java/org/nzbhydra/searching/CategoryProviderTest.java @@ -1,199 +1,202 @@ -package org.nzbhydra.searching; - -import org.junit.Before; -import org.junit.Test; -import org.nzbhydra.config.BaseConfig; -import org.nzbhydra.config.category.CategoriesConfig; -import org.nzbhydra.config.category.Category; -import org.nzbhydra.config.category.Category.Subtype; - -import java.util.*; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -public class CategoryProviderTest { - - CategoryProvider testee = new CategoryProvider(); - - @Before - public void setUp() throws Exception { - List categories = new ArrayList<>(); - Category category = new Category(); - category.setName("all"); - category.setNewznabCategories(Collections.emptyList()); - categories.add(category); - - category = new Category(); - category.setName("n/a"); - category.setNewznabCategories(Collections.emptyList()); - categories.add(category); - - category = new Category(); - category.setName("3000,3030"); - category.setNewznabCategories(Arrays.asList(Collections.singletonList(3000), Collections.singletonList(3030))); - categories.add(category); - - category = new Category(); - category.setName("4000"); - category.setNewznabCategories(Arrays.asList(Collections.singletonList(4000))); - categories.add(category); - - category = new Category(); - category.setName("4030"); - category.setNewznabCategories(Arrays.asList(Collections.singletonList(4030))); - categories.add(category); - - category = new Category(); - category.setName("4090"); - category.setSubtype(Subtype.COMIC); - category.setNewznabCategories(Arrays.asList(Collections.singletonList(4090))); - categories.add(category); - - category = new Category(); - category.setName("4090&11_000"); - category.setNewznabCategories(Arrays.asList(Arrays.asList(4090, 11_000))); - categories.add(category); - - category = new Category(); - category.setName("7070&77_000+8080&88_000"); - category.setNewznabCategories(Arrays.asList(Arrays.asList(7070, 77_000), Arrays.asList(8080, 88_000))); - categories.add(category); - - category = new Category(); - category.setName("10_000&20_000&30_000"); - category.setNewznabCategories(Arrays.asList(Arrays.asList(10_000, 20_000, 30_000))); - categories.add(category); - - category = new Category(); - category.setName("6060+9090&99_000"); - category.setNewznabCategories(Arrays.asList(Arrays.asList(6060), Arrays.asList(9090, 99_000))); - categories.add(category); - - - category = new Category(); - category.setName("7020,8010"); - category.setSubtype(Subtype.ANIME); - category.setNewznabCategories(Arrays.asList(Collections.singletonList(7020), Collections.singletonList(8010))); - categories.add(category); - BaseConfig baseConfig = new BaseConfig(); - CategoriesConfig categoriesConfig = new CategoriesConfig(); - categoriesConfig.setCategories(categories); - baseConfig.setCategoriesConfig(categoriesConfig); - testee.baseConfig = baseConfig; - testee.initialize(); - } - - @Test - public void shouldConvertSearchNewznabCategoriesToInternalCategory() throws Exception { - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(3000), CategoriesConfig.allCategory).getName(), is("3000,3030")); - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(3030), CategoriesConfig.allCategory).getName(), is("3000,3030")); - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7020), CategoriesConfig.allCategory).getName(), is("7020,8010")); - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7000, 7020), CategoriesConfig.allCategory).getName(), is("7020,8010")); - - //Different order - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7020, 8010), CategoriesConfig.allCategory).getName(), is("7020,8010")); - - //One general category - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4000), CategoriesConfig.allCategory).getName(), is("4000")); - - //Generalized (4020 matches 4000) - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4020), CategoriesConfig.allCategory).getName(), is("4000")); - - //Specific trumps general - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4090), CategoriesConfig.allCategory).getName(), is("4090")); - - //If a main category and a subcategory are supplied use the main category - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4000, 4090), CategoriesConfig.allCategory).getName(), is("4000")); - - //No matching found, use all - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7090), CategoriesConfig.allCategory).getName(), is("All")); - - //String input - assertThat(testee.fromSearchNewznabCategories("4000").getName(), is("4000")); - assertThat(testee.fromSearchNewznabCategories("7020,8010").getName(), is("7020,8010")); - - //No cats - assertThat(testee.fromSearchNewznabCategories(Collections.emptyList(), CategoriesConfig.allCategory).getName(), is("All")); - assertThat(testee.fromSearchNewznabCategories("").getName(), is("N/A")); - - //Two subcategories, both of which have a match -> Use the general one - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4030, 4090), CategoriesConfig.allCategory).getName(), is("4000")); - - //Two main categories -> Use the higher one (doesn't really matter, one needs to be used) - assertThat(testee.fromSearchNewznabCategories(Arrays.asList(3000, 4000), CategoriesConfig.allCategory).getName(), is("4000")); - } - - @Test - public void shouldConvertIndexerNewznabCategoriesToInternalCategory() throws Exception { - //Should return N/A on empty list - assertThat(testee.fromResultNewznabCategories(Collections.emptyList()).getName(), is("N/A")); - - //Should return more specific matching category - assertThat(testee.fromResultNewznabCategories(Arrays.asList(4000, 4090)).getName(), is("4090")); - - //Should ignore numbers from custom range if not specified in category - assertThat(testee.fromResultNewznabCategories(Arrays.asList(4000, 4090, 10_000)).getName(), is("4090")); - assertThat(testee.fromResultNewznabCategories(Arrays.asList(4000, 10_000)).getName(), is("4000")); - - //Should use category that matches custom range if specified - //Matches both and is more specific than only 4090 - assertThat(testee.fromResultNewznabCategories(Arrays.asList(4090, 11_000)).getName(), is("4090&11_000")); - //Doesn't use custom range but category with combined is still found - assertThat(testee.fromResultNewznabCategories(Arrays.asList(6060)).getName(), is("6060+9090&99_000")); - //Uses combined range and is found - assertThat(testee.fromResultNewznabCategories(Arrays.asList(6060, 99_000)).getName(), is("6060+9090&99_000")); - //Does not match both numbers in either combination - assertThat(testee.fromResultNewznabCategories(Arrays.asList(7070, 88_000)).getName(), is("N/A")); - //Uses combination of three - assertThat(testee.fromResultNewznabCategories(Arrays.asList(10_000, 20_000, 30_000)).getName(), is("10_000&20_000&30_000")); - assertThat(testee.fromResultNewznabCategories(Arrays.asList(10_000, 20_000)).getName(), is("N/A")); - - //Should return matching main category if subcat not found - assertThat(testee.fromResultNewznabCategories(Arrays.asList(4020)).getName(), is("4000")); - - //Should return N/A if no matching found - assertThat(testee.fromResultNewznabCategories(Arrays.asList(9999)).getName(), is("N/A")); - } - - - @Test - public void testcheckCategoryMatchingMainCategory() { - assertThat(testee.checkCategoryMatchingMainCategory(5030, 5000), is(true)); - assertThat(testee.checkCategoryMatchingMainCategory(5000, 5000), is(true)); - assertThat(testee.checkCategoryMatchingMainCategory(4030, 5000), is(false)); - assertThat(testee.checkCategoryMatchingMainCategory(4000, 5000), is(false)); - assertThat(testee.checkCategoryMatchingMainCategory(4030, 4030), is(false)); - } - - @Test - public void testThatWorksWithSameNewznabCategoriesAsCategory() { - testee.baseConfig.getCategoriesConfig().getCategories().clear(); - Category category = new Category(); - category.setName("TV HD"); - category.setNewznabCategories(Arrays.asList(Collections.singletonList(5010), Collections.singletonList(5040))); - testee.baseConfig.getCategoriesConfig().getCategories().add(category); - - Category category2 = new Category(); - category2.setName("TV UHD"); - category2.setNewznabCategories(Arrays.asList(Collections.singletonList(5045))); - testee.baseConfig.getCategoriesConfig().getCategories().add(category2); - - testee.initialize(); - - Category foundCategory = testee.getCategory(Arrays.asList(5010, 5040), null); - - assertThat(foundCategory.getName(), is(category.getName())); - } - - @Test - public void shouldFindBySubtype() { - Optional animeOptional = testee.fromSubtype(Subtype.ANIME); - assertThat(animeOptional.isPresent(), is(true)); - assertThat(animeOptional.get().getName(), is("7020,8010")); - - Optional magazineOptional = testee.fromSubtype(Subtype.MAGAZINE); - assertThat(magazineOptional.isPresent(), is(false)); - } - -} \ No newline at end of file +package org.nzbhydra.searching; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.category.CategoriesConfig; +import org.nzbhydra.config.category.Category; +import org.nzbhydra.config.category.Category.Subtype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CategoryProviderTest { + + CategoryProvider testee = new CategoryProvider(); + + @BeforeEach + public void setUp() throws Exception { + List categories = new ArrayList<>(); + Category category = new Category(); + category.setName("all"); + category.setNewznabCategories(Collections.emptyList()); + categories.add(category); + + category = new Category(); + category.setName("n/a"); + category.setNewznabCategories(Collections.emptyList()); + categories.add(category); + + category = new Category(); + category.setName("3000,3030"); + category.setNewznabCategories(Arrays.asList(Collections.singletonList(3000), Collections.singletonList(3030))); + categories.add(category); + + category = new Category(); + category.setName("4000"); + category.setNewznabCategories(Arrays.asList(Collections.singletonList(4000))); + categories.add(category); + + category = new Category(); + category.setName("4030"); + category.setNewznabCategories(Arrays.asList(Collections.singletonList(4030))); + categories.add(category); + + category = new Category(); + category.setName("4090"); + category.setSubtype(Subtype.COMIC); + category.setNewznabCategories(Arrays.asList(Collections.singletonList(4090))); + categories.add(category); + + category = new Category(); + category.setName("4090&11_000"); + category.setNewznabCategories(Arrays.asList(Arrays.asList(4090, 11_000))); + categories.add(category); + + category = new Category(); + category.setName("7070&77_000+8080&88_000"); + category.setNewznabCategories(Arrays.asList(Arrays.asList(7070, 77_000), Arrays.asList(8080, 88_000))); + categories.add(category); + + category = new Category(); + category.setName("10_000&20_000&30_000"); + category.setNewznabCategories(Arrays.asList(Arrays.asList(10_000, 20_000, 30_000))); + categories.add(category); + + category = new Category(); + category.setName("6060+9090&99_000"); + category.setNewznabCategories(Arrays.asList(Arrays.asList(6060), Arrays.asList(9090, 99_000))); + categories.add(category); + + + category = new Category(); + category.setName("7020,8010"); + category.setSubtype(Subtype.ANIME); + category.setNewznabCategories(Arrays.asList(Collections.singletonList(7020), Collections.singletonList(8010))); + categories.add(category); + BaseConfig baseConfig = new BaseConfig(); + CategoriesConfig categoriesConfig = new CategoriesConfig(); + categoriesConfig.setCategories(categories); + baseConfig.setCategoriesConfig(categoriesConfig); + testee.baseConfig = baseConfig; + testee.initialize(); + } + + @Test + void shouldConvertSearchNewznabCategoriesToInternalCategory() throws Exception { + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(3000), CategoriesConfig.allCategory).getName()).isEqualTo("3000,3030"); + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(3030), CategoriesConfig.allCategory).getName()).isEqualTo("3000,3030"); + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7020), CategoriesConfig.allCategory).getName()).isEqualTo("7020,8010"); + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7000, 7020), CategoriesConfig.allCategory).getName()).isEqualTo("7020,8010"); + + //Different order + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7020, 8010), CategoriesConfig.allCategory).getName()).isEqualTo("7020,8010"); + + //One general category + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4000), CategoriesConfig.allCategory).getName()).isEqualTo("4000"); + + //Generalized (4020 matches 4000) + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4020), CategoriesConfig.allCategory).getName()).isEqualTo("4000"); + + //Specific trumps general + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4090), CategoriesConfig.allCategory).getName()).isEqualTo("4090"); + + //If a main category and a subcategory are supplied use the main category + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4000, 4090), CategoriesConfig.allCategory).getName()).isEqualTo("4000"); + + //No matching found, use all + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(7090), CategoriesConfig.allCategory).getName()).isEqualTo("All"); + + //String input + assertThat(testee.fromSearchNewznabCategories("4000").getName()).isEqualTo("4000"); + assertThat(testee.fromSearchNewznabCategories("7020,8010").getName()).isEqualTo("7020,8010"); + + //No cats + assertThat(testee.fromSearchNewznabCategories(Collections.emptyList(), CategoriesConfig.allCategory).getName()).isEqualTo("All"); + assertThat(testee.fromSearchNewznabCategories("").getName()).isEqualTo("N/A"); + + //Two subcategories, both of which have a match -> Use the general one + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(4030, 4090), CategoriesConfig.allCategory).getName()).isEqualTo("4000"); + + //Two main categories -> Use the higher one (doesn't really matter, one needs to be used) + assertThat(testee.fromSearchNewznabCategories(Arrays.asList(3000, 4000), CategoriesConfig.allCategory).getName()).isEqualTo("4000"); + } + + @Test + void shouldConvertIndexerNewznabCategoriesToInternalCategory() throws Exception { + //Should return N/A on empty list + assertThat(testee.fromResultNewznabCategories(Collections.emptyList()).getName()).isEqualTo("N/A"); + + //Should return more specific matching category + assertThat(testee.fromResultNewznabCategories(Arrays.asList(4000, 4090)).getName()).isEqualTo("4090"); + + //Should ignore numbers from custom range if not specified in category + assertThat(testee.fromResultNewznabCategories(Arrays.asList(4000, 4090, 10_000)).getName()).isEqualTo("4090"); + assertThat(testee.fromResultNewznabCategories(Arrays.asList(4000, 10_000)).getName()).isEqualTo("4000"); + + //Should use category that matches custom range if specified + //Matches both and is more specific than only 4090 + assertThat(testee.fromResultNewznabCategories(Arrays.asList(4090, 11_000)).getName()).isEqualTo("4090&11_000"); + //Doesn't use custom range but category with combined is still found + assertThat(testee.fromResultNewznabCategories(Arrays.asList(6060)).getName()).isEqualTo("6060+9090&99_000"); + //Uses combined range and is found + assertThat(testee.fromResultNewznabCategories(Arrays.asList(6060, 99_000)).getName()).isEqualTo("6060+9090&99_000"); + //Does not match both numbers in either combination + assertThat(testee.fromResultNewznabCategories(Arrays.asList(7070, 88_000)).getName()).isEqualTo("N/A"); + //Uses combination of three + assertThat(testee.fromResultNewznabCategories(Arrays.asList(10_000, 20_000, 30_000)).getName()).isEqualTo("10_000&20_000&30_000"); + assertThat(testee.fromResultNewznabCategories(Arrays.asList(10_000, 20_000)).getName()).isEqualTo("N/A"); + + //Should return matching main category if subcat not found + assertThat(testee.fromResultNewznabCategories(Arrays.asList(4020)).getName()).isEqualTo("4000"); + + //Should return N/A if no matching found + assertThat(testee.fromResultNewznabCategories(Arrays.asList(9999)).getName()).isEqualTo("N/A"); + } + + + @Test + void testcheckCategoryMatchingMainCategory() { + assertThat(testee.checkCategoryMatchingMainCategory(5030, 5000)).isEqualTo(true); + assertThat(testee.checkCategoryMatchingMainCategory(5000, 5000)).isEqualTo(true); + assertThat(testee.checkCategoryMatchingMainCategory(4030, 5000)).isEqualTo(false); + assertThat(testee.checkCategoryMatchingMainCategory(4000, 5000)).isEqualTo(false); + assertThat(testee.checkCategoryMatchingMainCategory(4030, 4030)).isEqualTo(false); + } + + @Test + void testThatWorksWithSameNewznabCategoriesAsCategory() { + testee.baseConfig.getCategoriesConfig().getCategories().clear(); + Category category = new Category(); + category.setName("TV HD"); + category.setNewznabCategories(Arrays.asList(Collections.singletonList(5010), Collections.singletonList(5040))); + testee.baseConfig.getCategoriesConfig().getCategories().add(category); + + Category category2 = new Category(); + category2.setName("TV UHD"); + category2.setNewznabCategories(Arrays.asList(Collections.singletonList(5045))); + testee.baseConfig.getCategoriesConfig().getCategories().add(category2); + + testee.initialize(); + + Category foundCategory = testee.getCategory(Arrays.asList(5010, 5040), null); + + assertThat(foundCategory.getName()).isEqualTo(category.getName()); + } + + @Test + void shouldFindBySubtype() { + Optional animeOptional = testee.fromSubtype(Subtype.ANIME); + assertThat(animeOptional.isPresent()).isEqualTo(true); + assertThat(animeOptional.get().getName()).isEqualTo("7020,8010"); + + Optional magazineOptional = testee.fromSubtype(Subtype.MAGAZINE); + assertThat(magazineOptional.isPresent()).isEqualTo(false); + } + +} diff --git a/core/src/test/java/org/nzbhydra/searching/CustomQueryAndTitleMappingTest.java b/core/src/test/java/org/nzbhydra/searching/CustomQueryAndTitleCustomQueryAndTitleMappingHandlerTest.java similarity index 60% rename from core/src/test/java/org/nzbhydra/searching/CustomQueryAndTitleMappingTest.java rename to core/src/test/java/org/nzbhydra/searching/CustomQueryAndTitleCustomQueryAndTitleMappingHandlerTest.java index db50215c4..88765fb60 100644 --- a/core/src/test/java/org/nzbhydra/searching/CustomQueryAndTitleMappingTest.java +++ b/core/src/test/java/org/nzbhydra/searching/CustomQueryAndTitleCustomQueryAndTitleMappingHandlerTest.java @@ -19,20 +19,22 @@ package org.nzbhydra.searching; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.searching.CustomQueryAndTitleMapping; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.SearchRequest; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; -public class CustomQueryAndTitleMappingTest { +public class CustomQueryAndTitleCustomQueryAndTitleMappingHandlerTest { @InjectMocks - private CustomQueryAndTitleMapping testee = new CustomQueryAndTitleMapping(new BaseConfig()); + private CustomQueryAndTitleMappingHandler testee = new CustomQueryAndTitleMappingHandler(new BaseConfig()); @Test @@ -42,8 +44,8 @@ public class CustomQueryAndTitleMappingTest { searchRequest.setSeason(4); searchRequest.setEpisode("21"); - CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("SEARCH;QUERY;{title:my show name}{0:.*};{title} s{season:00}e{episode:00}"); - testee.mapSearchRequest(searchRequest, Collections.singletonList(mapping)); + CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("SEARCH;QUERY;{title:my show name}{0:.*};{title} s{season:00}e{episode:00}"); + testee.mapSearchRequest(searchRequest, Collections.singletonList(customQueryAndTitleMapping)); assertThat(searchRequest.getQuery()).isPresent().get().isEqualTo("my show name s04e21"); } @@ -52,8 +54,8 @@ public class CustomQueryAndTitleMappingTest { final SearchRequest searchRequest = new SearchRequest(); searchRequest.setQuery("my show name s4"); - CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("SEARCH;QUERY;{title:my show name}{0:.*};{title} s{season:00}e{episode:00}"); - testee.mapSearchRequest(searchRequest, Collections.singletonList(mapping)); + CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("SEARCH;QUERY;{title:my show name}{0:.*};{title} s{season:00}e{episode:00}"); + testee.mapSearchRequest(searchRequest, Collections.singletonList(customQueryAndTitleMapping)); assertThat(searchRequest.getQuery()).isPresent().get().isEqualTo("my show name s4"); } @@ -64,8 +66,8 @@ public class CustomQueryAndTitleMappingTest { searchRequest.setSeason(4); searchRequest.setEpisode("21"); - CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("SEARCH;QUERY;{title:my show name}{0:.*};some other title I want{0}"); - testee.mapSearchRequest(searchRequest, Collections.singletonList(mapping)); + CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("SEARCH;QUERY;{title:my show name}{0:.*};some other title I want{0}"); + testee.mapSearchRequest(searchRequest, Collections.singletonList(customQueryAndTitleMapping)); assertThat(searchRequest.getQuery()).isPresent().get().isEqualTo("some other title I want s4"); } @@ -74,8 +76,8 @@ public class CustomQueryAndTitleMappingTest { final SearchRequest searchRequest = new SearchRequest(); searchRequest.setQuery("my show name s4"); - CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("SEARCH;QUERY;my show name.*;some other title I want"); - testee.mapSearchRequest(searchRequest, Collections.singletonList(mapping)); + CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("SEARCH;QUERY;my show name.*;some other title I want"); + testee.mapSearchRequest(searchRequest, Collections.singletonList(customQueryAndTitleMapping)); assertThat(searchRequest.getQuery()).isPresent().get().isEqualTo("some other title I want"); } @@ -84,8 +86,8 @@ public class CustomQueryAndTitleMappingTest { final SearchRequest searchRequest = new SearchRequest(); searchRequest.setTitle("my show name"); - CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("SEARCH;TITLE;{title:my show name};some other title I want"); - testee.mapSearchRequest(searchRequest, Collections.singletonList(mapping)); + CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("SEARCH;TITLE;{title:my show name};some other title I want"); + testee.mapSearchRequest(searchRequest, Collections.singletonList(customQueryAndTitleMapping)); assertThat(searchRequest.getTitle()).isPresent().get().isEqualTo("some other title I want"); } @@ -94,25 +96,25 @@ public class CustomQueryAndTitleMappingTest { final SearchRequest searchRequest = new SearchRequest(); searchRequest.setQuery("my show name s4"); - final CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("SEARCH;QUERY;{title:my show name}{0:.*};{title} s{season:00}e{episode:00}"); - assertThat(testee.isDatasetMatch(CustomQueryAndTitleMapping.MetaData.fromSearchRequest(searchRequest), mapping)).isTrue(); + final CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("SEARCH;QUERY;{title:my show name}{0:.*};{title} s{season:00}e{episode:00}"); + assertTrue(testee.isDatasetMatch(CustomQueryAndTitleMappingHandler.MetaData.fromSearchRequest(searchRequest), customQueryAndTitleMapping)); searchRequest.setQuery("my show name whatever"); - assertThat(testee.isDatasetMatch(CustomQueryAndTitleMapping.MetaData.fromSearchRequest(searchRequest), mapping)).isTrue(); + assertTrue(testee.isDatasetMatch(CustomQueryAndTitleMappingHandler.MetaData.fromSearchRequest(searchRequest), customQueryAndTitleMapping)); searchRequest.setQuery("my other show name"); - assertThat(testee.isDatasetMatch(CustomQueryAndTitleMapping.MetaData.fromSearchRequest(searchRequest), mapping)).isFalse(); + assertThat(testee.isDatasetMatch(CustomQueryAndTitleMappingHandler.MetaData.fromSearchRequest(searchRequest), customQueryAndTitleMapping)).isFalse(); } @Test public void shouldFindMatchingDatasetTitle() { final SearchRequest searchRequest = new SearchRequest(); searchRequest.setTitle("my wrongly mapped title"); - final CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("TVSEARCH;TITLE;{title:my wrongly mapped title};my correct title"); - assertThat(testee.isDatasetMatch(CustomQueryAndTitleMapping.MetaData.fromSearchRequest(searchRequest), mapping)).isTrue(); + final CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("TVSEARCH;TITLE;{title:my wrongly mapped title};my correct title"); + assertTrue(testee.isDatasetMatch(CustomQueryAndTitleMappingHandler.MetaData.fromSearchRequest(searchRequest), customQueryAndTitleMapping)); searchRequest.setTitle("my correctly mapped title"); - assertThat(testee.isDatasetMatch(CustomQueryAndTitleMapping.MetaData.fromSearchRequest(searchRequest), mapping)).isFalse(); + assertThat(testee.isDatasetMatch(CustomQueryAndTitleMappingHandler.MetaData.fromSearchRequest(searchRequest), customQueryAndTitleMapping)).isFalse(); } @Test @@ -120,10 +122,10 @@ public class CustomQueryAndTitleMappingTest { final SearchRequest searchRequest = new SearchRequest(); searchRequest.setSearchType(SearchType.TVSEARCH); searchRequest.setTitle("Fairy Tail 49"); - final CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("TVSEARCH;TITLE;{title:Fairy Tail} {ep:[0-9]+};{title} german e{ep}"); - assertThat(testee.isDatasetMatch(CustomQueryAndTitleMapping.MetaData.fromSearchRequest(searchRequest), mapping)).isTrue(); + final CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("TVSEARCH;TITLE;{title:Fairy Tail} {ep:[0-9]+};{title} german e{ep}"); + assertTrue(testee.isDatasetMatch(CustomQueryAndTitleMappingHandler.MetaData.fromSearchRequest(searchRequest), customQueryAndTitleMapping)); - testee.mapSearchRequest(searchRequest, Collections.singletonList(mapping)); + testee.mapSearchRequest(searchRequest, Collections.singletonList(customQueryAndTitleMapping)); assertThat(searchRequest.getTitle()).isPresent().get().isEqualTo("Fairy Tail german e49"); } @@ -135,8 +137,8 @@ public class CustomQueryAndTitleMappingTest { item.getAttributes().put("season", "1"); item.getAttributes().put("episode", "2"); - final CustomQueryAndTitleMapping.Mapping mapping = new CustomQueryAndTitleMapping.Mapping("null;RESULT_TITLE;{title:.*};{title} {season:0} {episode:00}"); - final SearchResultItem newItem = testee.mapSearchResult(item, Collections.singletonList(mapping)); + final CustomQueryAndTitleMapping customQueryAndTitleMapping = new CustomQueryAndTitleMapping("null;RESULT_TITLE;{title:.*};{title} {season:0} {episode:00}"); + final SearchResultItem newItem = testee.mapSearchResult(item, Collections.singletonList(customQueryAndTitleMapping)); assertThat(newItem.getTitle()).isEqualTo("Fairy Tail 1 02"); } diff --git a/core/src/test/java/org/nzbhydra/searching/DuplicateDetectorTest.java b/core/src/test/java/org/nzbhydra/searching/DuplicateDetectorTest.java index a00d718ef..40b623c03 100644 --- a/core/src/test/java/org/nzbhydra/searching/DuplicateDetectorTest.java +++ b/core/src/test/java/org/nzbhydra/searching/DuplicateDetectorTest.java @@ -1,8 +1,8 @@ package org.nzbhydra.searching; import com.google.common.collect.Sets; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -23,6 +23,7 @@ import java.util.List; import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; public class DuplicateDetectorTest { @@ -32,7 +33,7 @@ public class DuplicateDetectorTest { private ConfigProvider configProviderMock; - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); BaseConfig baseConfig = new BaseConfig(); @@ -43,7 +44,7 @@ public class DuplicateDetectorTest { } @Test - public void shouldDetectThatTheSame() throws Exception { + void shouldDetectThatTheSame() throws Exception { SearchResultItem item1 = new SearchResultItem(); setValues(item1, "1", "poster", "group", Instant.now()); SearchResultItem item2 = new SearchResultItem(); @@ -60,7 +61,7 @@ public class DuplicateDetectorTest { } @Test - public void shouldWorkWithOneIndexerProvidingGroupAndPosterAndOneNot() throws Exception { + void shouldWorkWithOneIndexerProvidingGroupAndPosterAndOneNot() throws Exception { SearchResultItem item1 = new SearchResultItem(); setValues(item1, "Indexer1", "chuck@norris.com", "alt.binaries.triballs", Instant.ofEpochSecond(1447928064)); item1.setUsenetDate(item1.getPubDate()); @@ -104,7 +105,7 @@ public class DuplicateDetectorTest { @Test - public void duplicateIdsShouldBeSameForDuplicates() throws Exception { + void duplicateIdsShouldBeSameForDuplicates() throws Exception { SearchResultItem item1 = new SearchResultItem(); setValues(item1, "1", "poster1", "group", Instant.now()); SearchResultItem item2 = new SearchResultItem(); @@ -124,7 +125,7 @@ public class DuplicateDetectorTest { } @Test - public void shouldUseUsenetDateForComparison() throws Exception { + void shouldUseUsenetDateForComparison() throws Exception { SearchResultItem item1 = new SearchResultItem(); setValues(item1, "1", "poster1", "group", Instant.now()); item1.setPubDate(Instant.now().minus(100, ChronoUnit.DAYS)); @@ -134,7 +135,7 @@ public class DuplicateDetectorTest { item1.setUsenetDate(Instant.now()); item2.setUsenetDate(Instant.now()); - assertThat(testee.testForDuplicateAge(item1, item2, 1F)).isTrue(); + assertTrue(testee.testForDuplicateAge(item1, item2, 1F)); item2.setUsenetDate(null); assertThat(testee.testForDuplicateAge(item1, item2, 1F)).isFalse(); @@ -150,7 +151,7 @@ public class DuplicateDetectorTest { item.setGroup(group); item.setSize(10000L); item.setLink("" + new Random().nextInt()); - Newznab indexer = new Newznab(); + Newznab indexer = new Newznab(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); IndexerConfig config = new IndexerConfig(); config.setName(indexerName); IndexerEntity indexerEntity = new IndexerEntity(); diff --git a/core/src/test/java/org/nzbhydra/searching/IndexerForSearchSelectorTest.java b/core/src/test/java/org/nzbhydra/searching/IndexerForSearchSelectorTest.java index f6033dfb7..444e07bb8 100644 --- a/core/src/test/java/org/nzbhydra/searching/IndexerForSearchSelectorTest.java +++ b/core/src/test/java/org/nzbhydra/searching/IndexerForSearchSelectorTest.java @@ -1,472 +1,471 @@ -package org.nzbhydra.searching; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.internal.util.collections.Sets; -import org.nzbhydra.config.BaseConfig; -import org.nzbhydra.config.ConfigProvider; -import org.nzbhydra.config.SearchSourceRestriction; -import org.nzbhydra.config.SearchingConfig; -import org.nzbhydra.config.category.Category; -import org.nzbhydra.config.category.Category.Subtype; -import org.nzbhydra.config.indexer.IndexerConfig; -import org.nzbhydra.config.indexer.SearchModuleType; -import org.nzbhydra.downloading.FileDownloadRepository; -import org.nzbhydra.indexers.Indexer; -import org.nzbhydra.indexers.IndexerApiAccessRepository; -import org.nzbhydra.indexers.IndexerEntity; -import org.nzbhydra.indexers.status.IndexerLimit; -import org.nzbhydra.indexers.status.IndexerLimitRepository; -import org.nzbhydra.mediainfo.InfoProvider; -import org.nzbhydra.mediainfo.MediaIdType; -import org.nzbhydra.searching.dtoseventsenums.DownloadType; -import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; -import org.springframework.context.ApplicationEventPublisher; - -import javax.persistence.EntityManager; -import javax.persistence.Query; -import java.sql.Timestamp; -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertTrue; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -public class IndexerForSearchSelectorTest { - - @Mock - private IndexerApiAccessRepository indexerApiAccessRepository; - @Mock - private FileDownloadRepository nzbDownloadRepository; - @Mock - private SearchModuleProvider searchModuleProviderMock; - @Mock - private Indexer indexer; - @Mock - private IndexerEntity indexerEntity; - @Mock - private InfoProvider infoProviderMock; - @Mock - private SearchRequest searchRequest; - private IndexerConfig indexerConfigMock = new IndexerConfig(); - @Mock - private BaseConfig baseConfig; - @Mock - private ConfigProvider configProvider; - @Mock - private SearchingConfig searchingConfig; - @Mock - private ApplicationEventPublisher eventPublisher; - @Mock - private Category category; - @Mock - private EntityManager entityManagerMock; - @Mock - private Query queryMock; - @Mock - private IndexerLimitRepository indexerLimitRepositoryMock; - - - private Map count; - private final IndexerLimit indexerLimit = new IndexerLimit(); - - @InjectMocks - private IndexerForSearchSelector testee; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - count = new HashMap<>(); - when(searchModuleProviderMock.getIndexers()).thenReturn(Arrays.asList(indexer)); - when(configProvider.getBaseConfig()).thenReturn(baseConfig); - when(indexer.getConfig()).thenReturn(indexerConfigMock); - when(indexer.getName()).thenReturn("indexer"); - when(indexer.getIndexerEntity()).thenReturn(indexerEntity); - when(baseConfig.getSearching()).thenReturn(searchingConfig); - when(category.getName()).thenReturn("category"); - when(category.getSubtype()).thenReturn(Subtype.NONE); - when(entityManagerMock.createNativeQuery(anyString())).thenReturn(queryMock); - when(indexerLimitRepositoryMock.findByIndexer(any())).thenReturn(indexerLimit); - } - - - @Test - public void shouldCheckIfSelectedByUser() { - when(searchModuleProviderMock.getIndexers()).thenReturn(Arrays.asList(indexer)); - when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); - when(searchRequest.getIndexers()).thenReturn(Optional.of(Sets.newSet("anotherIndexer"))); - - assertFalse(testee.checkIndexerSelected(indexer)); - - when(searchRequest.getSource()).thenReturn(SearchSource.API); - assertFalse(testee.checkIndexerSelected(indexer)); - - when(searchRequest.getIndexers()).thenReturn(Optional.of(Sets.newSet("indexer"))); - - when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); - assertTrue(testee.checkIndexerSelected(indexer)); - - when(searchRequest.getSource()).thenReturn(SearchSource.API); - assertTrue(testee.checkIndexerSelected(indexer)); - } - - @Test - public void shouldCheckIfDisabledBySystem() { - when(searchingConfig.isIgnoreTemporarilyDisabled()).thenReturn(false); - - indexerConfigMock.setState(IndexerConfig.State.ENABLED); - indexerConfigMock.setDisabledUntil(null); - assertTrue(testee.checkIndexerStatus(indexer)); - - indexerConfigMock.setState(IndexerConfig.State.DISABLED_SYSTEM_TEMPORARY); - indexerConfigMock.setDisabledUntil(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); - assertFalse(testee.checkIndexerStatus(indexer)); - - indexerConfigMock.setState(IndexerConfig.State.DISABLED_SYSTEM); - assertFalse(testee.checkIndexerStatus(indexer)); - } - - @Test - public void shouldCheckForCategory() { - when(searchRequest.getCategory()).thenReturn(category); - indexerConfigMock.setEnabledCategories(Collections.emptyList()); - - assertTrue(testee.checkDisabledForCategory(indexer)); - - indexerConfigMock.setEnabledCategories(Arrays.asList("anotherCategory")); - assertFalse(testee.checkDisabledForCategory(indexer)); - - indexerConfigMock.setEnabledCategories(Arrays.asList(("category"))); - assertTrue(testee.checkDisabledForCategory(indexer)); - } - - @Test - public void shouldCheckForLoadLimiting() { - indexerConfigMock.setLoadLimitOnRandom(null); - assertTrue(testee.checkLoadLimiting(indexer)); - - indexerConfigMock.setLoadLimitOnRandom(1); - for (int i = 0; i < 50; i++) { - assertTrue(testee.checkLoadLimiting(indexer)); - } - - indexerConfigMock.setLoadLimitOnRandom(2); - int countNotPicked = 0; - for (int i = 0; i < 500; i++) { - countNotPicked += testee.checkLoadLimiting(indexer) ? 0 : 1; - } - assertTrue(countNotPicked > 0); - } - - @Test - public void shouldCheckContext() { - when(searchModuleProviderMock.getIndexers()).thenReturn(Arrays.asList(indexer)); - when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); - indexerConfigMock.setEnabledForSearchSource(SearchSourceRestriction.API); - - assertFalse(testee.checkSearchSource(indexer)); - - when(searchRequest.getSource()).thenReturn(SearchSource.API); - assertTrue(testee.checkSearchSource(indexer)); - } - - @Test - public void shouldCheckIdConversion() { - Set supported = Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB); - Set provided = Sets.newSet(MediaIdType.TVMAZE); - - when(searchRequest.getQuery()).thenReturn(Optional.empty()); - when(infoProviderMock.canConvertAny(provided, supported)).thenReturn(true); - assertTrue(testee.checkSearchId(indexer)); - - //Search ID doesn't matter if a query is provided - when(searchRequest.getQuery()).thenReturn(Optional.of("a query")); - when(infoProviderMock.canConvertAny(provided, supported)).thenReturn(false); - assertTrue(testee.checkSearchId(indexer)); - - //When no IDs are provided and no query is provided the ID check should be successful (might be an update query) - provided = new HashSet<>(); - when(searchRequest.getQuery()).thenReturn(Optional.empty()); - verify(infoProviderMock, never()).canConvertAny(provided, supported); - assertTrue(testee.checkSearchId(indexer)); - } - - @Test - public void shouldIgnoreHitAndDownloadLimitIfNoneAreSet() { - indexerConfigMock.setHitLimit(null); - indexerConfigMock.setDownloadLimit(null); - testee.checkIndexerHitLimit(indexer); - verify(nzbDownloadRepository, never()).findBySearchResultIndexerOrderByTimeDesc(any(), any()); - verify(indexerApiAccessRepository, never()).findByIndexerOrderByTimeDesc(any(), any()); - } - - @Test - public void shouldIgnoreHitLimitIfNotYetReached() { - indexerConfigMock.setHitLimit(10); - when(queryMock.getResultList()).thenReturn(Collections.emptyList()); - boolean result = testee.checkIndexerHitLimit(indexer); - assertTrue(result); - verify(entityManagerMock).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitRollingTimeWindowUsingAccessHistory() { - //Last access was yesterday afternoon, next possible hit should be today at afternoon - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(1); - indexerConfigMock.setHitLimitResetTime(null); - when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); - boolean result = testee.checkIndexerHitLimit(indexer); - assertFalse(result); - verify(entityManagerMock).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitFixedResetTimeLaterUsingAccessHistoryAboveLimit() { - //Last access was yesterday afternoon, fixed reset time is later today, should allow then - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(1); - indexerConfigMock.setHitLimitResetTime(12); - when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); - boolean result = testee.checkIndexerHitLimit(indexer); - assertFalse(result); - verify(entityManagerMock).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitFixedResetTimeLaterUsingAccessHistoryBelowLimit() { - //Last access was yesterday afternoon, fixed reset time is later today, should allow then - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(2); - indexerConfigMock.setHitLimitResetTime(12); - when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); - boolean result = testee.checkIndexerHitLimit(indexer); - assertTrue(result); - verify(entityManagerMock).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitFixedResetTimeReachedUsingAccessHistoryA() { - //Last access was yesterday afternoon, fixed reset time was today, should allow - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(1); - indexerConfigMock.setHitLimitResetTime(6); - when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); - boolean result = testee.checkIndexerHitLimit(indexer); - assertTrue(result); - verify(entityManagerMock).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitUsingApiHitsAndOldestHitAboveLimit() { - //Last access was yesterday afternoon, next possible hit should be today at afternoon - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(1); - indexerLimit.setApiHits(1); - indexerLimit.setOldestApiHit(firstAccess); - - boolean result = testee.checkIndexerHitLimit(indexer); - assertFalse(result); - verify(entityManagerMock, never()).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitUsingApiHitsAndOldestHitOlderThanOneDay() { - //Last access was yesterday afternoon, next possible hit should be today at afternoon - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-11T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(1); - indexerLimit.setApiHits(1); - indexerLimit.setOldestApiHit(firstAccess); - - boolean result = testee.checkIndexerHitLimit(indexer); - assertTrue(result); - verify(entityManagerMock, never()).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitUsingApiHitsAndOldestHitBelowlimit() { - //Last access was yesterday afternoon, next possible hit should be today at afternoon - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(2); - indexerLimit.setApiHits(1); - indexerLimit.setOldestApiHit(firstAccess); - - boolean result = testee.checkIndexerHitLimit(indexer); - assertTrue(result); - verify(entityManagerMock, never()).createNativeQuery(anyString()); - } - - @Test - public void shouldPreferApiResultOldestHitOverHistory() { - //Last access was yesterday afternoon, next possible hit should be today at afternoon - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - //A while ago - Instant oldestHitDatabase = Instant.parse("2021-01-01T16:00:00.000Z"); - Instant oldestHitApiResult = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(1); - indexerLimit.setApiHits(1); - when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(oldestHitDatabase))); - indexerLimit.setOldestApiHit(oldestHitApiResult); - - boolean result = testee.checkIndexerHitLimit(indexer); - assertFalse(result); - verify(entityManagerMock, never()).createNativeQuery(anyString()); - } - - @Test - public void shouldFollowApiHitLimitUsingApiHitsFromResponsAndOldestHitFromHistory() { - //Last access was yesterday afternoon, next possible hit should be today at afternoon - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimit(1); - indexerLimit.setApiHits(1); - when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); - - boolean result = testee.checkIndexerHitLimit(indexer); - - assertFalse(result); - verify(entityManagerMock).createNativeQuery(anyString()); - } - - @Test - public void shouldIgnoreDownloadLimitIfNotYetReachedUsingAccessHistory() { - indexerConfigMock.setDownloadLimit(10); - when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(Instant.now().minus(10, ChronoUnit.MILLIS)))); - boolean result = testee.checkIndexerHitLimit(indexer); - assertTrue(result); - } - - @Test - public void shouldCalculateNextHitWithRollingTimeWindows() throws Exception { - //First access was at 05:38, hit limit reset time is 10:00, now it's 09:00, next possible hit tomorrow at 05:38 - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-13T05:38:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimitResetTime(null); - - Instant nextHit = testee.calculateNextPossibleHit(indexerConfigMock, firstAccess).toInstant(ZoneOffset.UTC); - - assertThat(nextHit).isEqualTo(Instant.parse("2021-01-14T05:38:00.000Z")); - } - - @Test - public void shouldOnlyUseTorznabIndexersForTorrentSearches() throws Exception { - indexerConfigMock.setSearchModuleType(SearchModuleType.NEWZNAB); - when(searchRequest.getDownloadType()).thenReturn(org.nzbhydra.searching.dtoseventsenums.DownloadType.TORRENT); - assertFalse("Only torznab indexers should be used for torrent searches", testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer)); - - indexerConfigMock.setSearchModuleType(SearchModuleType.TORZNAB); - when(searchRequest.getDownloadType()).thenReturn(org.nzbhydra.searching.dtoseventsenums.DownloadType.TORRENT); - assertTrue("Torznab indexers should be used for torrent searches", testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer)); - - indexerConfigMock.setSearchModuleType(SearchModuleType.TORZNAB); - when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); - when(searchRequest.getDownloadType()).thenReturn(org.nzbhydra.searching.dtoseventsenums.DownloadType.NZB); - assertTrue("Torznab indexers should be selected for internal NZB searches", testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer)); - - indexerConfigMock.setSearchModuleType(SearchModuleType.TORZNAB); - when(searchRequest.getSource()).thenReturn(SearchSource.API); - when(searchRequest.getDownloadType()).thenReturn(DownloadType.NZB); - assertFalse("Torznab indexers should not be selected for API NZB searches", testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer)); - } - - @Test - public void shouldCalculateNextHitWithFixedResetTime() { - //First access was at 05:38, hit limit reset time is 10:00, now it's 09:00, next possible hit today at 10:00 - Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); - Instant firstAccess = Instant.parse("2021-01-13T05:38:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimitResetTime(10); - - Instant nextHit = testee.calculateNextPossibleHit(indexerConfigMock, firstAccess).toInstant(ZoneOffset.UTC); - - assertThat(nextHit).isEqualTo(Instant.parse("2021-01-13T10:00:00.000Z")); - - //First access was at 05:38, hit limit reset time is 10:00, now it's 14:00, next possible hit tomorrow at 04:00 - currentTime = Instant.parse("2021-01-13T14:00:00.000Z"); - testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); - indexerConfigMock.setHitLimitResetTime(4); - - nextHit = testee.calculateNextPossibleHit(indexerConfigMock, firstAccess).toInstant(ZoneOffset.UTC); - - assertThat(nextHit).isEqualTo(Instant.parse("2021-01-14T04:00:00.000Z")); - } - - - @Test - public void shouldHonorSchedule() throws Exception { - testee.clock = Clock.fixed(Instant.ofEpochSecond(1512974083), ZoneId.of("UTC")); //Monday, December 11, 2017 6:34:43 AM - assertThat(testee.isInTime("mo")).isTrue(); - assertThat(testee.isInTime("mo-tu")).isTrue(); - assertThat(testee.isInTime("mo-su")).isTrue(); - assertThat(testee.isInTime("tu-mo")).isTrue(); - assertThat(testee.isInTime("tu")).isFalse(); - assertThat(testee.isInTime("tu-we")).isFalse(); - - assertThat(testee.isInTime("mo6")).isTrue(); - assertThat(testee.isInTime("mo6-10")).isTrue(); - assertThat(testee.isInTime("mo6-23")).isTrue(); - assertThat(testee.isInTime("mo1-10")).isTrue(); - assertThat(testee.isInTime("mo1-5")).isFalse(); - assertThat(testee.isInTime("mo7-23")).isFalse(); - assertThat(testee.isInTime("mo22-8")).isTrue(); - assertThat(testee.isInTime("mo22-5")).isFalse(); - - assertThat(testee.isInTime("mo-tu6")).isTrue(); - assertThat(testee.isInTime("mo-tu6-10")).isTrue(); - assertThat(testee.isInTime("mo-tu1-2")).isFalse(); - assertThat(testee.isInTime("tu-we6")).isFalse(); - assertThat(testee.isInTime("tu-we6-10")).isFalse(); - - assertThat(testee.isInTime("6")).isTrue(); - assertThat(testee.isInTime("6-10")).isTrue(); - assertThat(testee.isInTime("7-10")).isFalse(); - assertThat(testee.isInTime("10-7")).isTrue(); - - indexerConfigMock.setSchedule(Arrays.asList("tu-we6", "mo")); - assertThat(testee.checkSchedule(indexer)).isTrue(); - - indexerConfigMock.setSchedule(Arrays.asList("tu-we6")); - assertThat(testee.checkSchedule(indexer)).isFalse(); - - testee.clock = Clock.fixed(Instant.ofEpochSecond(1513412203), ZoneId.of("UTC")); //Saturday, December 16, 2017 8:16:43 AM - assertThat(testee.isInTime("fr-sa")).isTrue(); - assertThat(testee.isInTime("mo-sa")).isTrue(); - } - - -} +package org.nzbhydra.searching; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.internal.util.collections.Sets; +import org.nzbhydra.config.BaseConfig; +import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.SearchSourceRestriction; +import org.nzbhydra.config.SearchingConfig; +import org.nzbhydra.config.category.Category; +import org.nzbhydra.config.category.Category.Subtype; +import org.nzbhydra.config.downloading.DownloadType; +import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.indexer.SearchModuleType; +import org.nzbhydra.config.mediainfo.MediaIdType; +import org.nzbhydra.downloading.FileDownloadRepository; +import org.nzbhydra.indexers.Indexer; +import org.nzbhydra.indexers.IndexerApiAccessRepository; +import org.nzbhydra.indexers.IndexerEntity; +import org.nzbhydra.indexers.status.IndexerLimit; +import org.nzbhydra.indexers.status.IndexerLimitRepository; +import org.nzbhydra.mediainfo.InfoProvider; +import org.nzbhydra.searching.searchrequests.SearchRequest; +import org.springframework.context.ApplicationEventPublisher; + +import java.sql.Timestamp; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +public class IndexerForSearchSelectorTest { + + @Mock + private IndexerApiAccessRepository indexerApiAccessRepository; + @Mock + private FileDownloadRepository nzbDownloadRepository; + @Mock + private SearchModuleProvider searchModuleProviderMock; + @Mock + private Indexer indexer; + private IndexerEntity indexerEntity = new IndexerEntity("indexer"); + @Mock + private InfoProvider infoProviderMock; + @Mock + private SearchRequest searchRequest; + private IndexerConfig indexerConfigMock = new IndexerConfig(); + @Mock + private BaseConfig baseConfig; + @Mock + private ConfigProvider configProvider; + @Mock + private SearchingConfig searchingConfig; + @Mock + private ApplicationEventPublisher eventPublisher; + @Mock + private Category category; + @Mock + private EntityManager entityManagerMock; + @Mock + private Query queryMock; + @Mock + private IndexerLimitRepository indexerLimitRepositoryMock; + + + private Map count; + private final IndexerLimit indexerLimit = new IndexerLimit(); + + @InjectMocks + private IndexerForSearchSelector testee; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + count = new HashMap<>(); + when(searchModuleProviderMock.getIndexers()).thenReturn(Arrays.asList(indexer)); + when(configProvider.getBaseConfig()).thenReturn(baseConfig); + when(indexer.getConfig()).thenReturn(indexerConfigMock); + when(indexer.getName()).thenReturn("indexer"); + when(indexer.getIndexerEntity()).thenReturn(indexerEntity); + when(baseConfig.getSearching()).thenReturn(searchingConfig); + when(category.getName()).thenReturn("category"); + when(category.getSubtype()).thenReturn(Subtype.NONE); + when(entityManagerMock.createNativeQuery(anyString())).thenReturn(queryMock); + when(indexerLimitRepositoryMock.findByIndexer(any())).thenReturn(indexerLimit); + when(searchRequest.meets(any())).thenCallRealMethod(); + } + + + @Test + void shouldCheckIfSelectedByUser() { + when(searchModuleProviderMock.getIndexers()).thenReturn(Arrays.asList(indexer)); + when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); + when(searchRequest.getIndexers()).thenReturn(Optional.of(Sets.newSet("anotherIndexer"))); + + assertThat(testee.checkIndexerSelected(indexer)).isFalse(); + + when(searchRequest.getSource()).thenReturn(SearchSource.API); + assertThat(testee.checkIndexerSelected(indexer)).isFalse(); + + when(searchRequest.getIndexers()).thenReturn(Optional.of(Sets.newSet("indexer"))); + + when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); + assertTrue(testee.checkIndexerSelected(indexer)); + + when(searchRequest.getSource()).thenReturn(SearchSource.API); + assertTrue(testee.checkIndexerSelected(indexer)); + } + + @Test + void shouldCheckIfDisabledBySystem() { + when(searchingConfig.isIgnoreTemporarilyDisabled()).thenReturn(false); + + indexerConfigMock.setState(IndexerConfig.State.ENABLED); + indexerConfigMock.setDisabledUntil(null); + assertTrue(testee.checkIndexerStatus(indexer)); + + indexerConfigMock.setState(IndexerConfig.State.DISABLED_SYSTEM_TEMPORARY); + indexerConfigMock.setDisabledUntil(Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli()); + assertThat(testee.checkIndexerStatus(indexer)).isFalse(); + + indexerConfigMock.setState(IndexerConfig.State.DISABLED_SYSTEM); + assertThat(testee.checkIndexerStatus(indexer)).isFalse(); + } + + @Test + void shouldCheckForCategory() { + when(searchRequest.getCategory()).thenReturn(category); + indexerConfigMock.setEnabledCategories(Collections.emptyList()); + + assertTrue(testee.checkDisabledForCategory(indexer)); + + indexerConfigMock.setEnabledCategories(Arrays.asList("anotherCategory")); + assertThat(testee.checkDisabledForCategory(indexer)).isFalse(); + + indexerConfigMock.setEnabledCategories(Arrays.asList(("category"))); + assertTrue(testee.checkDisabledForCategory(indexer)); + } + + @Test + void shouldCheckForLoadLimiting() { + indexerConfigMock.setLoadLimitOnRandom(null); + assertTrue(testee.checkLoadLimiting(indexer)); + + indexerConfigMock.setLoadLimitOnRandom(1); + for (int i = 0; i < 50; i++) { + assertTrue(testee.checkLoadLimiting(indexer)); + } + + indexerConfigMock.setLoadLimitOnRandom(2); + int countNotPicked = 0; + for (int i = 0; i < 500; i++) { + countNotPicked += testee.checkLoadLimiting(indexer) ? 0 : 1; + } + assertTrue(countNotPicked > 0); + } + + @Test + void shouldCheckContext() { + when(searchModuleProviderMock.getIndexers()).thenReturn(Arrays.asList(indexer)); + when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); + indexerConfigMock.setEnabledForSearchSource(SearchSourceRestriction.API); + + assertThat(testee.checkSearchSource(indexer)).isFalse(); + + when(searchRequest.getSource()).thenReturn(SearchSource.API); + assertTrue(testee.checkSearchSource(indexer)); + } + + @Test + void shouldCheckIdConversion() { + Set supported = Sets.newSet(MediaIdType.TVMAZE, MediaIdType.TVDB); + Set provided = Sets.newSet(MediaIdType.TVMAZE); + + when(searchRequest.getQuery()).thenReturn(Optional.empty()); + when(infoProviderMock.canConvertAny(provided, supported)).thenReturn(true); + assertTrue(testee.checkSearchId(indexer)); + + //Search ID doesn't matter if a query is provided + when(searchRequest.getQuery()).thenReturn(Optional.of("a query")); + when(infoProviderMock.canConvertAny(provided, supported)).thenReturn(false); + assertTrue(testee.checkSearchId(indexer)); + + //When no IDs are provided and no query is provided the ID check should be successful (might be an update query) + provided = new HashSet<>(); + when(searchRequest.getQuery()).thenReturn(Optional.empty()); + verify(infoProviderMock, never()).canConvertAny(provided, supported); + assertTrue(testee.checkSearchId(indexer)); + } + + @Test + void shouldIgnoreHitAndDownloadLimitIfNoneAreSet() { + indexerConfigMock.setHitLimit(null); + indexerConfigMock.setDownloadLimit(null); + testee.checkIndexerHitLimit(indexer); + verify(nzbDownloadRepository, never()).findBySearchResultIndexerOrderByTimeDesc(any(), any()); + } + + @Test + void shouldIgnoreHitLimitIfNotYetReached() { + indexerConfigMock.setHitLimit(10); + when(queryMock.getResultList()).thenReturn(Collections.emptyList()); + boolean result = testee.checkIndexerHitLimit(indexer); + assertTrue(result); + verify(entityManagerMock).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitRollingTimeWindowUsingAccessHistory() { + //Last access was yesterday afternoon, next possible hit should be today at afternoon + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(1); + indexerConfigMock.setHitLimitResetTime(null); + when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); + boolean result = testee.checkIndexerHitLimit(indexer); + assertThat(result).isFalse(); + verify(entityManagerMock).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitFixedResetTimeLaterUsingAccessHistoryAboveLimit() { + //Last access was yesterday afternoon, fixed reset time is later today, should allow then + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(1); + indexerConfigMock.setHitLimitResetTime(12); + when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); + boolean result = testee.checkIndexerHitLimit(indexer); + assertThat(result).isFalse(); + verify(entityManagerMock).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitFixedResetTimeLaterUsingAccessHistoryBelowLimit() { + //Last access was yesterday afternoon, fixed reset time is later today, should allow then + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(2); + indexerConfigMock.setHitLimitResetTime(12); + when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); + boolean result = testee.checkIndexerHitLimit(indexer); + assertTrue(result); + verify(entityManagerMock).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitFixedResetTimeReachedUsingAccessHistoryA() { + //Last access was yesterday afternoon, fixed reset time was today, should allow + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(1); + indexerConfigMock.setHitLimitResetTime(6); + when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); + boolean result = testee.checkIndexerHitLimit(indexer); + assertTrue(result); + verify(entityManagerMock).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitUsingApiHitsAndOldestHitAboveLimit() { + //Last access was yesterday afternoon, next possible hit should be today at afternoon + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(1); + indexerLimit.setApiHits(1); + indexerLimit.setOldestApiHit(firstAccess); + + boolean result = testee.checkIndexerHitLimit(indexer); + assertThat(result).isFalse(); + verify(entityManagerMock, never()).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitUsingApiHitsAndOldestHitOlderThanOneDay() { + //Last access was yesterday afternoon, next possible hit should be today at afternoon + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-11T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(1); + indexerLimit.setApiHits(1); + indexerLimit.setOldestApiHit(firstAccess); + + boolean result = testee.checkIndexerHitLimit(indexer); + assertTrue(result); + verify(entityManagerMock, never()).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitUsingApiHitsAndOldestHitBelowlimit() { + //Last access was yesterday afternoon, next possible hit should be today at afternoon + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(2); + indexerLimit.setApiHits(1); + indexerLimit.setOldestApiHit(firstAccess); + + boolean result = testee.checkIndexerHitLimit(indexer); + assertTrue(result); + verify(entityManagerMock, never()).createNativeQuery(anyString()); + } + + @Test + void shouldPreferApiResultOldestHitOverHistory() { + //Last access was yesterday afternoon, next possible hit should be today at afternoon + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + //A while ago + Instant oldestHitDatabase = Instant.parse("2021-01-01T16:00:00.000Z"); + Instant oldestHitApiResult = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(1); + indexerLimit.setApiHits(1); + when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(oldestHitDatabase))); + indexerLimit.setOldestApiHit(oldestHitApiResult); + + boolean result = testee.checkIndexerHitLimit(indexer); + assertThat(result).isFalse(); + verify(entityManagerMock, never()).createNativeQuery(anyString()); + } + + @Test + void shouldFollowApiHitLimitUsingApiHitsFromResponsAndOldestHitFromHistory() { + //Last access was yesterday afternoon, next possible hit should be today at afternoon + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-12T16:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimit(1); + indexerLimit.setApiHits(1); + when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(firstAccess))); + + boolean result = testee.checkIndexerHitLimit(indexer); + + assertThat(result).isFalse(); + verify(entityManagerMock).createNativeQuery(anyString()); + } + + @Test + void shouldIgnoreDownloadLimitIfNotYetReachedUsingAccessHistory() { + indexerConfigMock.setDownloadLimit(10); + when(queryMock.getResultList()).thenReturn(Arrays.asList(Timestamp.from(Instant.now().minus(10, ChronoUnit.MILLIS)))); + boolean result = testee.checkIndexerHitLimit(indexer); + assertTrue(result); + } + + @Test + void shouldCalculateNextHitWithRollingTimeWindows() throws Exception { + //First access was at 05:38, hit limit reset time is 10:00, now it's 09:00, next possible hit tomorrow at 05:38 + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-13T05:38:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimitResetTime(null); + + Instant nextHit = testee.calculateNextPossibleHit(indexerConfigMock, firstAccess).toInstant(ZoneOffset.UTC); + + assertThat(nextHit).isEqualTo(Instant.parse("2021-01-14T05:38:00.000Z")); + } + + @Test + void shouldOnlyUseTorznabIndexersForTorrentSearches() throws Exception { + indexerConfigMock.setSearchModuleType(SearchModuleType.NEWZNAB); + when(searchRequest.getDownloadType()).thenReturn(DownloadType.TORRENT); + assertFalse(testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer), "Only torznab indexers should be used for torrent searches"); + + indexerConfigMock.setSearchModuleType(SearchModuleType.TORZNAB); + when(searchRequest.getDownloadType()).thenReturn(DownloadType.TORRENT); + assertTrue(testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer), "Torznab indexers should be used for torrent searches"); + + indexerConfigMock.setSearchModuleType(SearchModuleType.TORZNAB); + when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); + when(searchRequest.getDownloadType()).thenReturn(DownloadType.NZB); + assertTrue(testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer), "Torznab indexers should be selected for internal NZB searches"); + + indexerConfigMock.setSearchModuleType(SearchModuleType.TORZNAB); + when(searchRequest.getSource()).thenReturn(SearchSource.API); + when(searchRequest.getDownloadType()).thenReturn(DownloadType.NZB); + assertFalse(testee.checkTorznabOnlyUsedForTorrentOrInternalSearches(indexer), "Torznab indexers should not be selected for API NZB searches"); + } + + @Test + void shouldCalculateNextHitWithFixedResetTime() { + //First access was at 05:38, hit limit reset time is 10:00, now it's 09:00, next possible hit today at 10:00 + Instant currentTime = Instant.parse("2021-01-13T09:00:00.000Z"); + Instant firstAccess = Instant.parse("2021-01-13T05:38:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimitResetTime(10); + + Instant nextHit = testee.calculateNextPossibleHit(indexerConfigMock, firstAccess).toInstant(ZoneOffset.UTC); + + assertThat(nextHit).isEqualTo(Instant.parse("2021-01-13T10:00:00.000Z")); + + //First access was at 05:38, hit limit reset time is 10:00, now it's 14:00, next possible hit tomorrow at 04:00 + currentTime = Instant.parse("2021-01-13T14:00:00.000Z"); + testee.clock = Clock.fixed(currentTime, ZoneId.of("UTC")); + indexerConfigMock.setHitLimitResetTime(4); + + nextHit = testee.calculateNextPossibleHit(indexerConfigMock, firstAccess).toInstant(ZoneOffset.UTC); + + assertThat(nextHit).isEqualTo(Instant.parse("2021-01-14T04:00:00.000Z")); + } + + + @Test + void shouldHonorSchedule() throws Exception { + testee.clock = Clock.fixed(Instant.ofEpochSecond(1512974083), ZoneId.of("UTC")); //Monday, December 11, 2017 6:34:43 AM + assertTrue(testee.isInTime("mo")); + assertTrue(testee.isInTime("mo-tu")); + assertTrue(testee.isInTime("mo-su")); + assertTrue(testee.isInTime("tu-mo")); + assertThat(testee.isInTime("tu")).isFalse(); + assertThat(testee.isInTime("tu-we")).isFalse(); + + assertTrue(testee.isInTime("mo6")); + assertTrue(testee.isInTime("mo6-10")); + assertTrue(testee.isInTime("mo6-23")); + assertTrue(testee.isInTime("mo1-10")); + assertThat(testee.isInTime("mo1-5")).isFalse(); + assertThat(testee.isInTime("mo7-23")).isFalse(); + assertTrue(testee.isInTime("mo22-8")); + assertThat(testee.isInTime("mo22-5")).isFalse(); + + assertTrue(testee.isInTime("mo-tu6")); + assertTrue(testee.isInTime("mo-tu6-10")); + assertThat(testee.isInTime("mo-tu1-2")).isFalse(); + assertThat(testee.isInTime("tu-we6")).isFalse(); + assertThat(testee.isInTime("tu-we6-10")).isFalse(); + + assertTrue(testee.isInTime("6")); + assertTrue(testee.isInTime("6-10")); + assertThat(testee.isInTime("7-10")).isFalse(); + assertTrue(testee.isInTime("10-7")); + + indexerConfigMock.setSchedule(Arrays.asList("tu-we6", "mo")); + assertTrue(testee.checkSchedule(indexer)); + + indexerConfigMock.setSchedule(Arrays.asList("tu-we6")); + assertThat(testee.checkSchedule(indexer)).isFalse(); + + testee.clock = Clock.fixed(Instant.ofEpochSecond(1513412203), ZoneId.of("UTC")); //Saturday, December 16, 2017 8:16:43 AM + assertTrue(testee.isInTime("fr-sa")); + assertTrue(testee.isInTime("mo-sa")); + } + + +} diff --git a/core/src/test/java/org/nzbhydra/searching/InternalSearchResultProcessorTest.java b/core/src/test/java/org/nzbhydra/searching/InternalSearchResultProcessorTest.java index fa05cf640..587939b9e 100644 --- a/core/src/test/java/org/nzbhydra/searching/InternalSearchResultProcessorTest.java +++ b/core/src/test/java/org/nzbhydra/searching/InternalSearchResultProcessorTest.java @@ -16,7 +16,7 @@ package org.nzbhydra.searching; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import org.nzbhydra.searching.dtoseventsenums.SearchResultWebTO; @@ -34,7 +34,7 @@ public class InternalSearchResultProcessorTest { @Test - public void setSearchResultDateRelatedValues() { + void setSearchResultDateRelatedValues() { SearchResultWebTOBuilder builder = SearchResultWebTO.builder(); SearchResultItem item = new SearchResultItem(); diff --git a/core/src/test/java/org/nzbhydra/searching/SearchEntityTest.java b/core/src/test/java/org/nzbhydra/searching/SearchEntityTest.java index 74a7ef145..85ee8b248 100644 --- a/core/src/test/java/org/nzbhydra/searching/SearchEntityTest.java +++ b/core/src/test/java/org/nzbhydra/searching/SearchEntityTest.java @@ -1,21 +1,25 @@ package org.nzbhydra.searching; import com.google.common.collect.Sets; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.nzbhydra.Jackson; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.searching.db.IdentifierKeyValuePair; import org.nzbhydra.searching.db.SearchEntity; +import org.nzbhydra.searching.db.SearchEntityTO; import java.time.Instant; import java.util.HashSet; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotEquals; public class SearchEntityTest { - private SearchEntity testee = new SearchEntity(); + private final SearchEntity testee = new SearchEntity(); @Test - public void getComparingHash() throws Exception { + void getComparingHash() throws Exception { testee.setTime(Instant.now()); testee.setQuery("query"); testee.setSeason(1); @@ -24,20 +28,39 @@ public class SearchEntityTest { testee.setTitle("title"); int hash = testee.getComparingHash(); testee.setTime(Instant.ofEpochMilli(100000L)); - assertEquals(hash, testee.getComparingHash()); + assertThat(testee.getComparingHash()).isEqualTo(hash); testee.setIdentifiers(Sets.newHashSet(new IdentifierKeyValuePair("key", "value"))); - assertEquals(hash, testee.getComparingHash()); + assertThat(testee.getComparingHash()).isEqualTo(hash); testee.setSeason(2); assertNotEquals(hash, testee.getComparingHash()); testee.setSeason(1); - assertEquals(hash, testee.getComparingHash()); + assertThat(testee.getComparingHash()).isEqualTo(hash); testee.setIdentifiers(new HashSet<>()); assertNotEquals(hash, testee.getComparingHash()); testee.setIdentifiers(Sets.newHashSet(new IdentifierKeyValuePair("key", "value"))); - assertEquals(hash, testee.getComparingHash()); + assertThat(testee.getComparingHash()).isEqualTo(hash); } + @Test + public void shouldBeConvertibleToTO() throws Exception { + testee.setTime(Instant.now()); + testee.setQuery("query"); + testee.setSeason(1); + testee.setEpisode("ep"); + testee.setIdentifiers(Sets.newHashSet(new IdentifierKeyValuePair("key", "value"))); + testee.setTitle("title"); + testee.setSearchType(SearchType.SEARCH); + testee.setSource(SearchSource.INTERNAL); + testee.setCategoryName("category"); + testee.setUsername("user"); + testee.setAuthor("author"); + testee.setIp("ip"); + testee.setUserAgent("userAgent"); + final SearchEntityTO to = Jackson.JSON_MAPPER.convertValue(testee, SearchEntityTO.class); + final String jsonTO = Jackson.JSON_MAPPER.writeValueAsString(to); + final String jsonEntity = Jackson.JSON_MAPPER.writeValueAsString(testee); + assertThat(jsonTO).isEqualTo(jsonEntity); + } - -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/searching/SearchResultAcceptorTest.java b/core/src/test/java/org/nzbhydra/searching/SearchResultAcceptorTest.java index f4e874e14..dabeeeed6 100644 --- a/core/src/test/java/org/nzbhydra/searching/SearchResultAcceptorTest.java +++ b/core/src/test/java/org/nzbhydra/searching/SearchResultAcceptorTest.java @@ -1,13 +1,14 @@ package org.nzbhydra.searching; import com.google.common.collect.HashMultiset; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.SearchSourceRestriction; import org.nzbhydra.config.SearchingConfig; import org.nzbhydra.config.category.Category; @@ -18,7 +19,6 @@ import org.nzbhydra.indexers.Newznab; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import org.nzbhydra.searching.searchrequests.InternalData; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -26,8 +26,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.Optional; -import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; public class SearchResultAcceptorTest { @@ -49,7 +50,7 @@ public class SearchResultAcceptorTest { @InjectMocks private SearchResultAcceptor testee = new SearchResultAcceptor(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(configProvider.getBaseConfig()).thenReturn(baseConfig); @@ -60,10 +61,11 @@ public class SearchResultAcceptorTest { when(searchRequest.getCategory()).thenReturn(category); item = new SearchResultItem(); item.setCategory(category); + when(searchRequest.meets(any())).thenCallRealMethod(); } @Test - public void shouldCheckForRequiredWords() throws Exception { + void shouldCheckForRequiredWords() throws Exception { internalData.getRequiredWords().clear(); internalData.getRequiredWords().add("abc.def"); item.setTitle("abc.def ghi"); @@ -73,9 +75,9 @@ public class SearchResultAcceptorTest { item.setTitle("abc.dEF ghi"); assertTrue(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); item.setTitle("abcdef ghi"); - assertFalse(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); + assertThat(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)).isFalse(); item.setTitle("abc def ghi"); - assertFalse(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); + assertThat(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)).isFalse(); internalData.getRequiredWords().clear(); internalData.getRequiredWords().add("abc"); @@ -84,15 +86,15 @@ public class SearchResultAcceptorTest { item.setTitle("abc.def ghi"); assertTrue(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); item.setTitle("abcdef ghi"); - assertFalse(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); + assertThat(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)).isFalse(); item.setTitle("def ghi"); - assertFalse(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); + assertThat(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)).isFalse(); internalData.getRequiredWords().add("def"); item.setTitle("abc def ghi"); assertTrue(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); item.setTitle("abc de"); - assertFalse(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)); + assertThat(testee.checkRequiredWords(HashMultiset.create(), internalData.getRequiredWords(), item, null)).isFalse(); internalData.getRequiredWords().add("def"); item.setTitle("abc def ghi"); @@ -105,15 +107,15 @@ public class SearchResultAcceptorTest { @Test - public void shouldCheckForForbiddenWords() throws Exception { + void shouldCheckForForbiddenWords() throws Exception { internalData.getForbiddenWords().clear(); internalData.getForbiddenWords().add("abc.def"); item.setTitle("abc.def ghi"); - assertFalse(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); + assertThat(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)).isFalse(); item.setTitle("abc.DEF ghi"); - assertFalse(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); + assertThat(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)).isFalse(); item.setTitle("abc.dEF ghi"); - assertFalse(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); + assertThat(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)).isFalse(); item.setTitle("abcdef ghi"); assertTrue(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); @@ -124,11 +126,11 @@ public class SearchResultAcceptorTest { internalData.getForbiddenWords().clear(); internalData.getForbiddenWords().add("abc"); item.setTitle("abc def ghi"); - assertFalse(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); + assertThat(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)).isFalse(); item.setTitle("ABC def ghi"); - assertFalse(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); + assertThat(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)).isFalse(); item.setTitle("aBC def ghi"); - assertFalse(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); + assertThat(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)).isFalse(); item.setTitle("abcdef ghi"); assertTrue(testee.checkForForbiddenWords(indexerConfig, HashMultiset.create(), internalData.getForbiddenWords(), item, null)); item.setTitle("def ghi"); @@ -136,7 +138,7 @@ public class SearchResultAcceptorTest { } @Test - public void shouldCheckForPassword() throws Exception { + void shouldCheckForPassword() throws Exception { when(searchingConfig.isIgnorePassworded()).thenReturn(false); SearchResultItem item = new SearchResultItem(); @@ -151,11 +153,11 @@ public class SearchResultAcceptorTest { assertTrue(testee.checkForPassword(HashMultiset.create(), item)); item.setPassworded(true); - assertFalse(testee.checkForPassword(HashMultiset.create(), item)); + assertThat(testee.checkForPassword(HashMultiset.create(), item)).isFalse(); } @Test - public void shouldCheckForAge() { + void shouldCheckForAge() { when(searchRequest.getMinage()).thenReturn(Optional.of(10)); when(searchRequest.getMaxage()).thenReturn(Optional.of(100)); SearchResultItem item = new SearchResultItem(); @@ -164,14 +166,14 @@ public class SearchResultAcceptorTest { assertTrue(testee.checkForAge(searchRequest, HashMultiset.create(), item)); item.setPubDate(Instant.now().minus(5, ChronoUnit.DAYS)); - assertFalse(testee.checkForAge(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForAge(searchRequest, HashMultiset.create(), item)).isFalse(); item.setPubDate(Instant.now().plus(105, ChronoUnit.DAYS)); - assertFalse(testee.checkForAge(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForAge(searchRequest, HashMultiset.create(), item)).isFalse(); } @Test - public void shouldCheckForSize() { + void shouldCheckForSize() { when(searchRequest.getMinsize()).thenReturn(Optional.of(10)); when(searchRequest.getMaxsize()).thenReturn(Optional.of(100)); SearchResultItem item = new SearchResultItem(); @@ -181,10 +183,10 @@ public class SearchResultAcceptorTest { assertTrue(testee.checkForSize(searchRequest, HashMultiset.create(), item)); item.setSize(5 * 1024 * 1024L); - assertFalse(testee.checkForSize(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForSize(searchRequest, HashMultiset.create(), item)).isFalse(); item.setSize(105 * 1024 * 1024L); - assertFalse(testee.checkForSize(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForSize(searchRequest, HashMultiset.create(), item)).isFalse(); //Apply limits for API searches when(searchRequest.getMinsize()).thenReturn(Optional.empty()); @@ -193,7 +195,7 @@ public class SearchResultAcceptorTest { category.setMaxSizePreset(1); category.setApplySizeLimitsToApi(true); when(searchRequest.getSource()).thenReturn(SearchSource.API); - assertFalse(testee.checkForSize(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForSize(searchRequest, HashMultiset.create(), item)).isFalse(); when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); assertTrue(testee.checkForSize(searchRequest, HashMultiset.create(), item)); @@ -201,13 +203,13 @@ public class SearchResultAcceptorTest { category.setMinSizePreset(200); category.setApplySizeLimitsToApi(true); when(searchRequest.getSource()).thenReturn(SearchSource.API); - assertFalse(testee.checkForSize(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForSize(searchRequest, HashMultiset.create(), item)).isFalse(); when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); assertTrue(testee.checkForSize(searchRequest, HashMultiset.create(), item)); } @Test - public void shouldCheckForForbiddenPoster() { + void shouldCheckForForbiddenPoster() { when(searchingConfig.getForbiddenPosters()).thenReturn(Arrays.asList("spammer")); item.setPoster("niceGuy"); @@ -217,14 +219,14 @@ public class SearchResultAcceptorTest { assertTrue(testee.checkForForbiddenPoster(HashMultiset.create(), item)); item.setPoster("spammer"); - assertFalse(testee.checkForForbiddenPoster(HashMultiset.create(), item)); + assertThat(testee.checkForForbiddenPoster(HashMultiset.create(), item)).isFalse(); when(searchingConfig.getForbiddenPosters()).thenReturn(Arrays.asList()); assertTrue(testee.checkForForbiddenPoster(HashMultiset.create(), item)); } @Test - public void shouldCheckForForbiddenGroup() { + void shouldCheckForForbiddenGroup() { when(searchingConfig.getForbiddenGroups()).thenReturn(Arrays.asList("spammergroup")); item.setGroup("niceGroup"); @@ -234,39 +236,39 @@ public class SearchResultAcceptorTest { assertTrue(testee.checkForForbiddenGroup(HashMultiset.create(), item)); item.setGroup("spammergroup"); - assertFalse(testee.checkForForbiddenGroup(HashMultiset.create(), item)); + assertThat(testee.checkForForbiddenGroup(HashMultiset.create(), item)).isFalse(); when(searchingConfig.getForbiddenGroups()).thenReturn(Collections.emptyList()); assertTrue(testee.checkForForbiddenGroup(HashMultiset.create(), item)); } @Test - public void shouldCheckForForbiddenCategory() { + void shouldCheckForForbiddenCategory() { category.setIgnoreResultsFrom(SearchSourceRestriction.BOTH); when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); - assertFalse(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)).isFalse(); when(searchRequest.getSource()).thenReturn(SearchSource.API); - assertFalse(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)).isFalse(); category.setIgnoreResultsFrom(SearchSourceRestriction.API); when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); assertTrue(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)); when(searchRequest.getSource()).thenReturn(SearchSource.API); - assertFalse(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)).isFalse(); category.setIgnoreResultsFrom(SearchSourceRestriction.INTERNAL); when(searchRequest.getSource()).thenReturn(SearchSource.INTERNAL); - assertFalse(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)).isFalse(); when(searchRequest.getSource()).thenReturn(SearchSource.API); assertTrue(testee.checkForCategoryShouldBeIgnored(searchRequest, HashMultiset.create(), item)); } @Test - public void shouldCheckForCategoryDisabledForIndexer() { - Indexer indexer = new Newznab(); + void shouldCheckForCategoryDisabledForIndexer() { + Indexer indexer = new Newznab(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); indexer.initialize(indexerConfig, new IndexerEntity()); item.setIndexer(indexer); @@ -280,31 +282,31 @@ public class SearchResultAcceptorTest { //Only other category enabled when(indexerConfig.getEnabledCategories()).thenReturn(Arrays.asList("Other")); - assertFalse(testee.checkForCategoryDisabledForIndexer(searchRequest, HashMultiset.create(), item)); + assertThat(testee.checkForCategoryDisabledForIndexer(searchRequest, HashMultiset.create(), item)).isFalse(); } @Test - public void shouldCheckRegexes() { + void shouldCheckRegexes() { item.setTitle("aabccd"); assertTrue(testee.checkRegexes(item, HashMultiset.create(), "", "")); assertTrue(testee.checkRegexes(item, HashMultiset.create(), "a+b", "")); assertTrue(testee.checkRegexes(item, HashMultiset.create(), "", "")); - assertFalse(testee.checkRegexes(item, HashMultiset.create(), "a+b", "c+d")); - assertFalse(testee.checkRegexes(item, HashMultiset.create(), "", "c+d")); + assertThat(testee.checkRegexes(item, HashMultiset.create(), "a+b", "c+d")).isFalse(); + assertThat(testee.checkRegexes(item, HashMultiset.create(), "", "c+d")).isFalse(); item.setTitle("My.favorite.Show.s01e03.720p.HDTV.mkv"); assertTrue(testee.checkRegexes(item, HashMultiset.create(), "720p.HDTV", "")); assertTrue(testee.checkRegexes(item, HashMultiset.create(), "", "SDTV")); - assertFalse(testee.checkRegexes(item, HashMultiset.create(), "", "720p.HDTV")); - assertFalse(testee.checkRegexes(item, HashMultiset.create(), "Show", "720p.HDTV")); + assertThat(testee.checkRegexes(item, HashMultiset.create(), "", "720p.HDTV")).isFalse(); + assertThat(testee.checkRegexes(item, HashMultiset.create(), "Show", "720p.HDTV")).isFalse(); item.setTitle("My.favorite.camera.Show.s01e03.720p.HDTV.mkv"); assertTrue(testee.checkRegexes(item, HashMultiset.create(), "", "\\.(SDTV|CAM)\\.")); assertTrue(testee.checkRegexes(item, HashMultiset.create(), "(720p|1080p).*.mkv$", "")); item.setTitle("My.favorite.camera.Show.s01e03.720p.HDTV.avi"); - assertFalse(testee.checkRegexes(item, HashMultiset.create(), "(720p|1080p).*.mkv$", "")); + assertThat(testee.checkRegexes(item, HashMultiset.create(), "(720p|1080p).*.mkv$", "")).isFalse(); item.setTitle("My.movie.about.mkv.avi"); - assertFalse(testee.checkRegexes(item, HashMultiset.create(), "\\.mkv$", "")); + assertThat(testee.checkRegexes(item, HashMultiset.create(), "\\.mkv$", "")).isFalse(); } diff --git a/core/src/test/java/org/nzbhydra/searching/SearchResultItemTest.java b/core/src/test/java/org/nzbhydra/searching/SearchResultItemTest.java index 73ee91b81..e33335f4d 100644 --- a/core/src/test/java/org/nzbhydra/searching/SearchResultItemTest.java +++ b/core/src/test/java/org/nzbhydra/searching/SearchResultItemTest.java @@ -1,33 +1,33 @@ package org.nzbhydra.searching; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Comparator; -import static org.assertj.core.api.Assertions.assertThat; - -public class SearchResultItemTest { - - @Test - public void compareTo() throws Exception { - Comparator comparator = SearchResultItem.comparator(); - SearchResultItem item1 = new SearchResultItem(); - item1.setPubDate(Instant.now()); - SearchResultItem item2 = new SearchResultItem(); - assertThat(comparator.compare(item1, item2)).isEqualTo(1); - assertThat(comparator.compare(item2, item1)).isEqualTo(-1); - item1.setPubDate(null); - assertThat(comparator.compare(item2, item1)).isEqualTo(0); - - item1.setPubDate(Instant.now()); - item2.setPubDate(item1.getPubDate()); - assertThat(comparator.compare(item2, item1)).isEqualTo(0); - - item1.setPubDate(Instant.now().minus(1, ChronoUnit.DAYS)); - assertThat(comparator.compare(item1, item2)).isEqualTo(-1); +import static org.assertj.core.api.Assertions.assertThat; + +public class SearchResultItemTest { + + @Test + void compareTo() throws Exception { + Comparator comparator = SearchResultItem.comparator(); + SearchResultItem item1 = new SearchResultItem(); + item1.setPubDate(Instant.now()); + SearchResultItem item2 = new SearchResultItem(); + assertThat(comparator.compare(item1, item2)).isEqualTo(1); + assertThat(comparator.compare(item2, item1)).isEqualTo(-1); + item1.setPubDate(null); + assertThat(comparator.compare(item2, item1)).isEqualTo(0); + + item1.setPubDate(Instant.now()); + item2.setPubDate(item1.getPubDate()); + assertThat(comparator.compare(item2, item1)).isEqualTo(0); + + item1.setPubDate(Instant.now().minus(1, ChronoUnit.DAYS)); + assertThat(comparator.compare(item1, item2)).isEqualTo(-1); } } \ No newline at end of file diff --git a/core/src/test/java/org/nzbhydra/searching/SearcherUnitTest.java b/core/src/test/java/org/nzbhydra/searching/SearcherUnitTest.java index b5b53c747..70391b151 100644 --- a/core/src/test/java/org/nzbhydra/searching/SearcherUnitTest.java +++ b/core/src/test/java/org/nzbhydra/searching/SearcherUnitTest.java @@ -4,9 +4,9 @@ import com.google.common.collect.HashMultiset; import com.google.common.collect.Lists; import com.google.common.collect.Multiset; import com.google.common.collect.Sets; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -16,8 +16,10 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigProvider; +import org.nzbhydra.config.SearchSource; import org.nzbhydra.config.category.Category; import org.nzbhydra.config.indexer.IndexerConfig; +import org.nzbhydra.config.searching.SearchType; import org.nzbhydra.indexers.Indexer; import org.nzbhydra.indexers.IndexerEntity; import org.nzbhydra.indexers.IndexerSearchEntity; @@ -30,10 +32,8 @@ import org.nzbhydra.searching.db.SearchResultRepository; import org.nzbhydra.searching.dtoseventsenums.DuplicateDetectionResult; import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult; import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; -import org.nzbhydra.searching.dtoseventsenums.SearchType; import org.nzbhydra.searching.searchrequests.InternalData; import org.nzbhydra.searching.searchrequests.SearchRequest; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; import org.springframework.context.ApplicationEventPublisher; import java.time.Instant; @@ -46,12 +46,11 @@ import java.util.Random; import java.util.Set; import java.util.stream.Collectors; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.*; -//@RunWith(SpringRunner.class) +//@ExtendWith(SpringExtension.class) //@ContextConfiguration(classes = {Searcher.class, DuplicateDetector.class}) public class SearcherUnitTest { @@ -72,12 +71,11 @@ public class SearcherUnitTest { private IndexerSearchResult searchResultMock2; @Mock private SearchRepository searchRepositoryMock; - @Mock - private IndexerEntity indexerEntity; + + private IndexerEntity indexerEntity = new IndexerEntity(); @Mock private SearchResultRepository searchResultRepositoryMock; - @Mock - private SearchResultEntity searchResultEntityMock; + private SearchResultEntity searchResultEntityMock = new SearchResultEntity(); @Mock private InfoProvider infoProviderMock; @Mock @@ -92,8 +90,7 @@ public class SearcherUnitTest { private ArgumentCaptor> searchResultItemsCaptor; @Mock private IndexerForSearchSelection pickingResultMock; - @Mock - private IndexerSearchEntity indexerSearchEntityMock; + private IndexerSearchEntity indexerSearchEntityMock = new IndexerSearchEntity(); @Mock private ApplicationEventPublisher applicationEventPublisherMock; @Mock @@ -101,10 +98,10 @@ public class SearcherUnitTest { private Random random = new Random(); - @Before + @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); - when(searchResultEntityMock.getIndexer()).thenReturn(indexerEntity); + searchResultEntityMock.setIndexer(indexerEntity); searcher.duplicateDetector = duplicateDetector; when(indexer1.getName()).thenReturn("indexer1"); @@ -139,32 +136,32 @@ public class SearcherUnitTest { } - @Ignore //TODO FIX + @Disabled //TODO FIX @Test - public void shouldFollowOffsetAndLimit() throws Exception { + void shouldFollowOffsetAndLimit() throws Exception { when(indexer1.search(any(), anyInt(), anyInt())).thenReturn(mockIndexerSearchResult(0, 2, true, 200, indexer1)); SearchRequest searchRequest = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 1); searchRequest.setTitle("some title so it will be found in the search request cache"); SearchResult result = searcher.search(searchRequest); List foundResults = result.getSearchResultItems(); - assertThat(foundResults.size(), is(1)); - assertThat(foundResults.get(0).getTitle(), is("item0")); + assertThat(foundResults.size()).isEqualTo(1); + assertThat(foundResults.get(0).getTitle()).isEqualTo("item0"); searchRequest.setOffset(1); result = searcher.search(searchRequest); foundResults = result.getSearchResultItems(); - assertThat(foundResults.size(), is(1)); - assertThat(foundResults.get(0).getTitle(), is("item1")); + assertThat(foundResults.size()).isEqualTo(1); + assertThat(foundResults.get(0).getTitle()).isEqualTo("item1"); verify(indexer1).search(any(), eq(0), any()); verify(indexer1, times(1)).search(any(), anyInt(), any()); } - @Ignore //TODO FIX + @Disabled //TODO FIX @Test - public void shouldReturnNewestFirst() throws Exception { + void shouldReturnNewestFirst() throws Exception { when(pickingResultMock.getSelectedIndexers()).thenReturn(Arrays.asList(indexer1, indexer2)); Instant now = Instant.now(); IndexerSearchResult indexer1results = mockIndexerSearchResult(0, 100, true, 100, indexer1); @@ -178,17 +175,17 @@ public class SearcherUnitTest { searchRequest.setTitle("some title so it will be found in the search request cache"); SearchResult result = searcher.search(searchRequest); List foundResults = result.getSearchResultItems(); - assertThat(foundResults.size(), is(2)); - assertThat(foundResults.get(0).getTitle(), is("item0")); - assertThat(foundResults.get(0).getIndexer(), is(indexer1)); - assertThat(foundResults.get(1).getTitle(), is("item0")); - assertThat(foundResults.get(1).getIndexer(), is(indexer2)); + assertThat(foundResults.size()).isEqualTo(2); + assertThat(foundResults.get(0).getTitle()).isEqualTo("item0"); + assertThat(foundResults.get(0).getIndexer()).isEqualTo(indexer1); + assertThat(foundResults.get(1).getTitle()).isEqualTo("item0"); + assertThat(foundResults.get(1).getIndexer()).isEqualTo(indexer2); } - @Ignore //TODO FIX + @Disabled //TODO FIX @Test - public void shouldWorkWithRejectedItems() throws Exception { + void shouldWorkWithRejectedItems() throws Exception { IndexerSearchResult result1 = mockIndexerSearchResult(0, 9, true, 20, indexer1); Multiset reasons = HashMultiset.create(); reasons.add("foobar"); @@ -200,20 +197,20 @@ public class SearcherUnitTest { searchRequest.setTitle("some title so it will be found in the search request cache"); SearchResult result = searcher.search(searchRequest); List foundResults = result.getSearchResultItems(); - assertThat(foundResults.size(), is(10)); + assertThat(foundResults.size()).isEqualTo(10); searchRequest.setOffset(10); searchRequest.setLimit(100); result = searcher.search(searchRequest); foundResults = result.getSearchResultItems(); - assertThat(foundResults.size(), is(9)); + assertThat(foundResults.size()).isEqualTo(9); verify(indexer1).search(any(), eq(0), any()); verify(indexer1, times(2)).search(any(), anyInt(), any()); } @Test - public void shouldPageCorrectly() throws Exception { + void shouldPageCorrectly() throws Exception { IndexerSearchResult result1a = mockIndexerSearchResult(0, 100, true, 200, indexer1); setResultsPerDay(0, result1a); diff --git a/core/src/test/java/org/nzbhydra/searching/searchrequests/SearchRequestTest.java b/core/src/test/java/org/nzbhydra/searching/searchrequests/SearchRequestTest.java index 24dcd7743..ab9164b1f 100644 --- a/core/src/test/java/org/nzbhydra/searching/searchrequests/SearchRequestTest.java +++ b/core/src/test/java/org/nzbhydra/searching/searchrequests/SearchRequestTest.java @@ -1,30 +1,30 @@ package org.nzbhydra.searching.searchrequests; -import org.junit.Before; -import org.junit.Test; -import org.nzbhydra.searching.dtoseventsenums.SearchType; -import org.nzbhydra.searching.searchrequests.SearchRequest.SearchSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.nzbhydra.config.SearchSource; +import org.nzbhydra.config.searching.SearchType; -import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; public class SearchRequestTest { private SearchRequest testee; - @Before + @BeforeEach public void setUp() { testee = new SearchRequest(SearchSource.INTERNAL, SearchType.SEARCH, 0, 100); } @Test - public void shouldFindAndRemoveExclusions() { + void shouldFindAndRemoveExclusions() { testee.setQuery("one two --three --four"); testee = testee.extractForbiddenWords(); - assertEquals(2, testee.getInternalData().getForbiddenWords().size()); - assertEquals("one two", testee.getQuery().get()); + assertThat(testee.getInternalData().getForbiddenWords()).hasSize(2); + assertThat(testee.getQuery().get()).isEqualTo("one two"); assertTrue(testee.getInternalData().getForbiddenWords().contains("three")); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/update/UpdateManagerTest.java b/core/src/test/java/org/nzbhydra/update/UpdateManagerTest.java index 18d5711a8..6a9de5247 100644 --- a/core/src/test/java/org/nzbhydra/update/UpdateManagerTest.java +++ b/core/src/test/java/org/nzbhydra/update/UpdateManagerTest.java @@ -3,8 +3,9 @@ package org.nzbhydra.update; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; -import org.junit.Before; -import org.junit.Test; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -23,9 +24,8 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; -import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -47,10 +47,10 @@ public class UpdateManagerTest { private ObjectMapper objectMapper; private BaseConfig baseConfig; - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - objectMapper = new ObjectMapper(); + objectMapper = new ObjectMapper(new YAMLFactory()); objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); baseConfig = new BaseConfig(); @@ -102,41 +102,40 @@ public class UpdateManagerTest { @Test - public void testThatChecksForUpdateAvailable() throws Exception { + void testThatChecksForUpdateAvailable() throws Exception { assertTrue(testee.isUpdateAvailable()); testee.currentVersion = new SemanticVersion("v2.0.0"); - assertFalse(testee.isUpdateAvailable()); + assertThat(testee.isUpdateAvailable()).isFalse(); } @Test - public void testThatChecksForUpdateAvailableWithPrerelease() throws Exception { + void testThatChecksForUpdateAvailableWithPrerelease() throws Exception { assertTrue(testee.isUpdateAvailable()); configProviderMock.getBaseConfig().getMain().setUpdateToPrereleases(true); testee.currentVersion = new SemanticVersion("v2.3.4"); - assertFalse(testee.isUpdateAvailable()); + assertThat(testee.isUpdateAvailable()).isFalse(); } - @Test - public void shouldGetChangesForVersion() throws Exception { + void shouldGetChangesForVersion() throws Exception { //Ensures that when a final version is available the changelog retrieved for the footer also includes the changes from the beta versions before that final version when(webAccessMock.callUrl(eq("http:/127.0.0.1:7070/changelog"))).thenReturn( - objectMapper.writeValueAsString(Arrays.asList( - new ChangelogVersionEntry("2.0.1", null, false, Arrays.asList(new ChangelogChangeEntry("note", "this is a newer prerelease"))), - new ChangelogVersionEntry("2.0.0", null, true, Arrays.asList(new ChangelogChangeEntry("note", "Next final release"))), - new ChangelogVersionEntry("1.0.1", null, false, Arrays.asList(new ChangelogChangeEntry("note", "A betal release"))), - new ChangelogVersionEntry("1.0.0", null, true, Arrays.asList(new ChangelogChangeEntry("note", "Initial final release"))) - ))); + objectMapper.writeValueAsString(Arrays.asList( + new ChangelogVersionEntry("2.0.1", null, false, Arrays.asList(new ChangelogChangeEntry("note", "this is a newer prerelease"))), + new ChangelogVersionEntry("2.0.0", null, true, Arrays.asList(new ChangelogChangeEntry("note", "Next final release"))), + new ChangelogVersionEntry("1.0.1", null, false, Arrays.asList(new ChangelogChangeEntry("note", "A betal release"))), + new ChangelogVersionEntry("1.0.0", null, true, Arrays.asList(new ChangelogChangeEntry("note", "Initial final release"))) + ))); testee.currentVersionString = "1.0.0"; //Should show changes for 1.01 and 2.0.0 List changesSince = testee.getChangesBetweenCurrentVersionAnd(new SemanticVersion("2.0.0")); - assertEquals(2, changesSince.size()); - assertEquals("2.0.0", changesSince.get(0).getVersion()); - assertEquals("1.0.1", changesSince.get(1).getVersion()); + assertThat(changesSince).hasSize(2); + assertThat(changesSince.get(0).getVersion()).isEqualTo("2.0.0"); + assertThat(changesSince.get(1).getVersion()).isEqualTo("1.0.1"); } diff --git a/core/src/test/java/org/nzbhydra/web/UrlCalculatorTest.java b/core/src/test/java/org/nzbhydra/web/UrlCalculatorTest.java index c6a10f997..402109edb 100644 --- a/core/src/test/java/org/nzbhydra/web/UrlCalculatorTest.java +++ b/core/src/test/java/org/nzbhydra/web/UrlCalculatorTest.java @@ -16,22 +16,21 @@ package org.nzbhydra.web; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.web.util.UriComponentsBuilder; -import javax.servlet.http.HttpServletRequest; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; //Doesn't work anymore after moving to spring boot 2.2 (See #506) -@Ignore +@Disabled public class UrlCalculatorTest { @Mock @@ -42,13 +41,13 @@ public class UrlCalculatorTest { private UrlCalculator testee = new UrlCalculator(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); } @Test - public void shouldReturnNewBuilderEachTime() { + void shouldReturnNewBuilderEachTime() { prepareConfig(false, false, "/"); prepareHeaders("127.0.0.1:5076", null, null, null); prepareServlet("http://127.0.0.1:5076", "127.0.0.1", 5076, "http", "/"); @@ -64,7 +63,7 @@ public class UrlCalculatorTest { @Test - public void shouldBuildCorrectlyForLocalAccessWithHttp() { + void shouldBuildCorrectlyForLocalAccessWithHttp() { prepareConfig(false, false, "/"); prepareHeaders("127.0.0.1:5076", null, null, null); prepareServlet("http://127.0.0.1:5076", "127.0.0.1", 5076, "http", "/"); @@ -77,7 +76,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForLocalAccessWithContextPath() { + void shouldBuildCorrectlyForLocalAccessWithContextPath() { prepareConfig(false, false, "/nzbhydra2"); prepareHeaders("127.0.0.1:5076", null, null, null); prepareServlet("http://127.0.0.1:5076", "127.0.0.1", 5076, "http", "/nzbhydra2"); @@ -91,7 +90,7 @@ public class UrlCalculatorTest { @Test - public void shouldBuildCorrectlyForLocalAccessWithBindAllAccessedViaLocalhost() { + void shouldBuildCorrectlyForLocalAccessWithBindAllAccessedViaLocalhost() { prepareConfig(false, true, "/"); prepareHeaders("127.0.0.1:5076", null, null, null); prepareServlet("http://127.0.0.1:5076", "127.0.0.1", 5076, "http", "/"); @@ -104,7 +103,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForLocalAccessWithBindAllAccessedViaNetworkAddress() { + void shouldBuildCorrectlyForLocalAccessWithBindAllAccessedViaNetworkAddress() { prepareConfig(false, true, "/"); prepareHeaders("192.168.1.111:5076", null, null, null); prepareServlet("http://192.168.1.111:5076", "192.168.1.111", 5076, "http", "/"); @@ -117,7 +116,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForReverseProxyWithHttpAccessedViaLocalhost() { + void shouldBuildCorrectlyForReverseProxyWithHttpAccessedViaLocalhost() { prepareConfig(false, false, "/nzbhydra2"); prepareHeaders("127.0.0.1", "127.0.0.1:4001", null, null); //nginx doesn't include the port in the "host" header prepareServlet("http://127.0.0.1:4001", "127.0.0.1", 80, "http", "/nzbhydra2"); //nginx reports port 80 in the servlet @@ -130,7 +129,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForReverseProxyWithHttpAccessedViaNetworkAddress() { + void shouldBuildCorrectlyForReverseProxyWithHttpAccessedViaNetworkAddress() { prepareConfig(false, false, "/nzbhydra2"); prepareHeaders("192.168.1.111", "192.168.1.111:4001", null, null); //nginx doesn't include the port in the "host" header prepareServlet("192.168.1.111:4001", "192.168.1.111", 80, "http", "/nzbhydra2"); //nginx reports port 80 in the servlet @@ -143,7 +142,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForReverseProxySendingForwardedPort() { + void shouldBuildCorrectlyForReverseProxySendingForwardedPort() { prepareConfig(false, false, "/nzbhydra2"); prepareHeaders("192.168.1.111", "192.168.1.111", null, "4001"); //x-forwarded-host may not contain the port prepareServlet("192.168.1.111:4001", "192.168.1.111", 80, "http", "/nzbhydra2"); //nginx reports port 80 in the servlet @@ -156,7 +155,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForReverseProxyWithHttpsAccessedViaLocalhost() { + void shouldBuildCorrectlyForReverseProxyWithHttpsAccessedViaLocalhost() { prepareConfig(false, false, "/nzbhydra2"); prepareHeaders("127.0.0.1:4001", "127.0.0.1:4001", "https", null); prepareServlet("http://127.0.0.1:4001", "127.0.0.1", 80, "http", "/nzbhydra2"); //nginx reports port 80 and scheme http in the servlet @@ -169,7 +168,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForReverseProxyWithHttpsAccessedViaNetworkAddress() { + void shouldBuildCorrectlyForReverseProxyWithHttpsAccessedViaNetworkAddress() { prepareConfig(false, false, "/nzbhydra2"); prepareHeaders("192.168.1.111:4001", "192.168.1.111:4001", "https", null); prepareServlet("192.168.1.111:4001", "192.168.1.111", 80, "http", "/nzbhydra2"); //nginx reports port 80 and scheme in the servlet @@ -182,7 +181,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForReverseProxyWithHttpsOnPort443() { + void shouldBuildCorrectlyForReverseProxyWithHttpsOnPort443() { prepareConfig(false, false, "/nzbhydra2"); prepareHeaders("localhost", "localhost", "https", null); prepareServlet("localhost", "localhost", 443, "http", "/nzbhydra2"); //nginx reports port 80 and scheme in the servlet @@ -195,7 +194,7 @@ public class UrlCalculatorTest { } @Test - public void shouldBuildCorrectlyForReverseProxyWithHttpOnPort80AndNoPath() { + void shouldBuildCorrectlyForReverseProxyWithHttpOnPort80AndNoPath() { prepareConfig(false, false, "/"); prepareHeaders("localhost", "localhost", "http", null); prepareServlet("localhost", "localhost", 80, "http", "/"); //nginx reports port 80 and scheme in the servlet @@ -229,4 +228,4 @@ public class UrlCalculatorTest { when(environmentMock.getProperty("server.port")).thenReturn("5076"); when(environmentMock.getProperty("server.servlet.context-path")).thenReturn(contextPath == null ? "/" : contextPath); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactoryTest.java b/core/src/test/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactoryTest.java index 2f1be337e..711802fc3 100644 --- a/core/src/test/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactoryTest.java +++ b/core/src/test/java/org/nzbhydra/webaccess/HydraOkHttp3ClientHttpRequestFactoryTest.java @@ -1,15 +1,15 @@ package org.nzbhydra.webaccess; import okhttp3.OkHttpClient; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigChangedEvent; import org.nzbhydra.config.ConfigProvider; -import org.nzbhydra.config.downloading.ProxyType; +import org.nzbhydra.config.ProxyType; import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory.SockProxySocketFactory; import java.net.InetSocketAddress; @@ -17,10 +17,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; public class HydraOkHttp3ClientHttpRequestFactoryTest { @@ -34,7 +31,7 @@ public class HydraOkHttp3ClientHttpRequestFactoryTest { BaseConfig baseConfig = new BaseConfig(); - @Before + @BeforeEach public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(configProviderMock.getBaseConfig()).thenReturn(baseConfig); @@ -47,70 +44,84 @@ public class HydraOkHttp3ClientHttpRequestFactoryTest { } @Test - public void shouldRecognizeLocalIps() { - assertThat(testee.isUriToBeIgnoredByProxy("localhost"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("127.0.0.1"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("192.168.1.1"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("192.168.240.3"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("10.0.240.3"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("8.8.8.8"), is(false)); + void shouldRecognizeLocalIps() { + assertThat(testee.isUriToBeIgnoredByProxy("localhost")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("127.0.0.1")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("192.168.1.1")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("192.168.240.3")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("10.0.240.3")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("8.8.8.8")).isEqualTo(false); } @Test - public void shouldRecognizeIgnoredDomains() { - assertThat(testee.isUriToBeIgnoredByProxy("mydomain.com"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("github.com"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("subdomain.otherdomain.net"), is(true)); - assertThat(testee.isUriToBeIgnoredByProxy("subdomain.otherDOmain.NET"), is(true)); + void shouldRecognizeIgnoredDomains() { + assertThat(testee.isUriToBeIgnoredByProxy("mydomain.com")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("github.com")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("subdomain.otherdomain.net")).isEqualTo(true); + assertThat(testee.isUriToBeIgnoredByProxy("subdomain.otherDOmain.NET")).isEqualTo(true); - assertThat(testee.isUriToBeIgnoredByProxy("subdomain.otherdomain.ORG"), is(false)); - assertThat(testee.isUriToBeIgnoredByProxy("somedomain.com"), is(false)); + assertThat(testee.isUriToBeIgnoredByProxy("subdomain.otherdomain.ORG")).isEqualTo(false); + assertThat(testee.isUriToBeIgnoredByProxy("somedomain.com")).isEqualTo(false); } @Test - public void shouldRecognizeSameHost() { - assertThat(Ssl.isSameHost("localhost", "localhost"), is(true)); - assertThat(Ssl.isSameHost("www.google.com", "google.com"), is(true)); - assertThat(Ssl.isSameHost("www.google.com", "localhost"), is(false)); + void shouldRecognizeSameHost() { + assertThat(Ssl.isSameHost("localhost", "localhost")).isEqualTo(true); + assertThat(Ssl.isSameHost("www.google.com", "google.com")).isEqualTo(true); + assertThat(Ssl.isSameHost("www.google.com", "localhost")).isEqualTo(false); } @Test - public void shouldNotUseProxyIfNotConfigured() throws URISyntaxException { + void shouldNotUseProxyIfNotConfigured() throws URISyntaxException { baseConfig.getMain().setProxyType(ProxyType.NONE); - OkHttpClient client = testee.getOkHttpClientBuilder(new URI("http://www.google.de")).build(); - assertThat(client.socketFactory() instanceof SockProxySocketFactory, is(false)); - assertThat(client.proxy(), is(nullValue())); + final URI requestUri = new URI("http://www.google.de"); + OkHttpClient client = testee.getOkHttpClientBuilder(requestUri.getHost()).build(); + assertThat(client.socketFactory() instanceof SockProxySocketFactory).isEqualTo(false); + assertThat(client.proxy()).isNull(); } @Test - public void shouldUseHttpProxyIfConfigured() throws URISyntaxException { + void shouldUseHttpProxyIfConfigured() throws URISyntaxException { baseConfig.getMain().setProxyType(ProxyType.HTTP); baseConfig.getMain().setProxyHost("proxyhost"); baseConfig.getMain().setProxyPort(1234); - OkHttpClient client = testee.getOkHttpClientBuilder(new URI("http://www.google.de")).build(); - assertThat(client.proxy().address(), equalTo(new InetSocketAddress("proxyhost", 1234))); + final URI requestUri = new URI("http://www.google.de"); + OkHttpClient client = testee.getOkHttpClientBuilder(requestUri.getHost()).build(); + assertThat(client.proxy().address()).isEqualTo(new InetSocketAddress("proxyhost", 1234)); } @Test - public void shouldUseSocksProxyIfConfigured() throws URISyntaxException { + void shouldUseSocksProxyIfConfigured() throws URISyntaxException { baseConfig.getMain().setProxyType(ProxyType.SOCKS); baseConfig.getMain().setProxyHost("proxyhost"); testee.handleConfigChangedEvent(new ConfigChangedEvent(this, baseConfig, baseConfig)); - OkHttpClient client = testee.getOkHttpClientBuilder(new URI("http://www.google.de")).build(); - assertThat(client.socketFactory() instanceof SockProxySocketFactory, is(true)); - assertThat(((SockProxySocketFactory) client.socketFactory()).host, is("proxyhost")); - assertThat(((SockProxySocketFactory) client.socketFactory()).username, is(nullValue())); - assertThat(((SockProxySocketFactory) client.socketFactory()).password, is(nullValue())); + final URI requestUri = new URI("http://www.google.de"); + OkHttpClient client = testee.getOkHttpClientBuilder(requestUri.getHost()).build(); + assertThat(client.socketFactory() instanceof SockProxySocketFactory).isEqualTo(true); + assertThat(((SockProxySocketFactory) client.socketFactory()).host).isEqualTo("proxyhost"); + assertThat(((SockProxySocketFactory) client.socketFactory()).username).isNull(); + assertThat(((SockProxySocketFactory) client.socketFactory()).password).isNull(); baseConfig.getMain().setProxyUsername("user"); baseConfig.getMain().setProxyPassword("pass"); testee.handleConfigChangedEvent(new ConfigChangedEvent(this, baseConfig, baseConfig)); - client = testee.getOkHttpClientBuilder(new URI("http://www.google.de")).build(); - assertThat(((SockProxySocketFactory) client.socketFactory()).username, is("user")); - assertThat(((SockProxySocketFactory) client.socketFactory()).password, is("pass")); + final URI requestUri1 = new URI("http://www.google.de"); + client = testee.getOkHttpClientBuilder(requestUri1.getHost()).build(); + assertThat(((SockProxySocketFactory) client.socketFactory()).username).isEqualTo("user"); + assertThat(((SockProxySocketFactory) client.socketFactory()).password).isEqualTo("pass"); + } + + @Test + public void shouldCacheClients() { + final String googleHost = "http://www.google.de"; + final String yahooHost = "http://www.yaboo.de"; + assertThat(testee.getOkHttpClient(googleHost)).isSameAs(testee.getOkHttpClient(googleHost)); + assertThat(testee.getOkHttpClient(googleHost, 1)).isSameAs(testee.getOkHttpClient(googleHost, 1)); + assertThat(testee.getOkHttpClient(googleHost, 1)).isNotSameAs(testee.getOkHttpClient(googleHost, 2)); + assertThat(testee.getOkHttpClient(googleHost)).isNotSameAs(testee.getOkHttpClient(yahooHost)); } -} \ No newline at end of file +} diff --git a/core/src/test/resources/config/application.properties b/core/src/test/resources/config/application.properties index 8a85963b2..1067cea66 100644 --- a/core/src/test/resources/config/application.properties +++ b/core/src/test/resources/config/application.properties @@ -3,9 +3,7 @@ spring.application.name=NZBHydra2 javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT=true jaxb.formatted.output=true # DataSource -#spring.datasource.url=jdbc:hsqldb:hsql://localhost:9001/xdb;shutdown=true -#spring.jpa.show-sql=true -#spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=YEAR,DATA,KEY endpoints.shutdown.enabled=true endpoints.restart.enabled=true spring.jpa.properties.hibernate.jdbc.batch_size=50 @@ -23,4 +21,6 @@ spring.jpa.generate-ddl=true spring.jpa.hibernate.ddl-auto=create main.port=1000 -main.host=127.0.0.1 \ No newline at end of file +main.host=127.0.0.1 + +main.urlBase=/ diff --git a/core/ui-src/html/custom-mapping-help.html b/core/ui-src/html/custom-mapping-help.html index b6b3e65af..763299447 100644 --- a/core/ui-src/html/custom-mapping-help.html +++ b/core/ui-src/html/custom-mapping-help.html @@ -1,11 +1,11 @@