├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── problem.md └── workflows │ ├── codeql.yml │ ├── housekeeping.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── manami-app ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── manamiproject │ │ │ └── manami │ │ │ └── app │ │ │ ├── Manami.kt │ │ │ ├── ManamiApp.kt │ │ │ ├── cache │ │ │ ├── AnimeCache.kt │ │ │ ├── Cache.kt │ │ │ ├── CacheEntry.kt │ │ │ ├── DefaultAnimeCache.kt │ │ │ ├── loader │ │ │ │ ├── CacheLoader.kt │ │ │ │ ├── DependentCacheLoader.kt │ │ │ │ └── SimpleCacheLoader.kt │ │ │ └── populator │ │ │ │ ├── AnimeCachePopulator.kt │ │ │ │ ├── CachePopulator.kt │ │ │ │ ├── CachePopulatorFinishedEvent.kt │ │ │ │ ├── DeadEntriesCachePopulator.kt │ │ │ │ └── NumberOfEntriesPerMetaDataProviderEvent.kt │ │ │ ├── commands │ │ │ ├── Command.kt │ │ │ ├── GenericReversibleCommand.kt │ │ │ ├── ReversibleCommand.kt │ │ │ └── history │ │ │ │ ├── CommandHistory.kt │ │ │ │ ├── DefaultCommandHistory.kt │ │ │ │ ├── EventStream.kt │ │ │ │ ├── FileSavedStatusChangedEvent.kt │ │ │ │ ├── SimpleEventStream.kt │ │ │ │ └── UndoRedoStatusEvent.kt │ │ │ ├── events │ │ │ ├── Event.kt │ │ │ ├── EventBus.kt │ │ │ ├── EventfulList.kt │ │ │ ├── SimpleEventBus.kt │ │ │ └── Subscribe.kt │ │ │ ├── extensions │ │ │ └── CollectionExtensions.kt │ │ │ ├── file │ │ │ ├── CmdNewFile.kt │ │ │ ├── CmdOpenFile.kt │ │ │ ├── DefaultFileHandler.kt │ │ │ ├── DefaultFileWriter.kt │ │ │ ├── FileHandler.kt │ │ │ ├── FileOpenedEvent.kt │ │ │ ├── FileParser.kt │ │ │ ├── FileWriter.kt │ │ │ ├── ManamiVersionHandler.kt │ │ │ ├── ParsedManamiFile.kt │ │ │ ├── Parser.kt │ │ │ └── SavedAsFileEvent.kt │ │ │ ├── inconsistencies │ │ │ ├── DefaultInconsistenciesHandler.kt │ │ │ ├── InconsistenciesCheckFinishedEvent.kt │ │ │ ├── InconsistenciesHandler.kt │ │ │ ├── InconsistenciesProgressEvent.kt │ │ │ ├── InconsistenciesSearchConfig.kt │ │ │ ├── InconsistencyHandler.kt │ │ │ ├── animelist │ │ │ │ ├── deadentries │ │ │ │ │ ├── AnimeListDeadEntriesInconsistenciesHandler.kt │ │ │ │ │ ├── AnimeListDeadEntriesInconsistenciesResult.kt │ │ │ │ │ └── AnimeListDeadEntriesInconsistenciesResultEvent.kt │ │ │ │ ├── episodes │ │ │ │ │ ├── AnimeListEpisodesInconsistenciesHandler.kt │ │ │ │ │ ├── AnimeListEpisodesInconsistenciesResult.kt │ │ │ │ │ ├── AnimeListEpisodesInconsistenciesResultEvent.kt │ │ │ │ │ └── EpisodeDiff.kt │ │ │ │ └── metadata │ │ │ │ │ ├── AnimeListMetaDataInconsistenciesHandler.kt │ │ │ │ │ ├── AnimeListMetaDataInconsistenciesResult.kt │ │ │ │ │ └── AnimeListMetaDataInconsistenciesResultEvent.kt │ │ │ └── lists │ │ │ │ ├── deadentries │ │ │ │ ├── CmdFixDeadEntries.kt │ │ │ │ ├── DeadEntriesInconsistenciesResult.kt │ │ │ │ ├── DeadEntriesInconsistenciesResultEvent.kt │ │ │ │ └── DeadEntriesInconsistencyHandler.kt │ │ │ │ └── metadata │ │ │ │ ├── CmdFixMetaData.kt │ │ │ │ ├── MetaDataDiff.kt │ │ │ │ ├── MetaDataInconsistenciesResult.kt │ │ │ │ ├── MetaDataInconsistenciesResultEvent.kt │ │ │ │ └── MetaDataInconsistencyHandler.kt │ │ │ ├── lists │ │ │ ├── AnimeEntry.kt │ │ │ ├── DefaultListHandler.kt │ │ │ ├── LinkEntry.kt │ │ │ ├── ListChangedEvent.kt │ │ │ ├── ListHandler.kt │ │ │ ├── animelist │ │ │ │ ├── AnimeListEntry.kt │ │ │ │ ├── CmdAddAnimeListEntry.kt │ │ │ │ ├── CmdRemoveAnimeListEntry.kt │ │ │ │ └── CmdReplaceAnimeListEntry.kt │ │ │ ├── ignorelist │ │ │ │ ├── AddIgnoreListStatusUpdateEvent.kt │ │ │ │ ├── CmdAddIgnoreListEntry.kt │ │ │ │ ├── CmdRemoveIgnoreListEntry.kt │ │ │ │ └── IgnoreListEntry.kt │ │ │ └── watchlist │ │ │ │ ├── AddWatchListStatusUpdateEvent.kt │ │ │ │ ├── CmdAddWatchListEntry.kt │ │ │ │ ├── CmdRemoveWatchListEntry.kt │ │ │ │ └── WatchListEntry.kt │ │ │ ├── migration │ │ │ ├── CmdMigrateEntries.kt │ │ │ ├── CmdRemoveUnmappedMigrationEntries.kt │ │ │ ├── DefaultMetaDataMigrationHandler.kt │ │ │ ├── MetaDataMigrationHandler.kt │ │ │ ├── MetaDataMigrationProgressEvent.kt │ │ │ └── MetaDataMigrationResultEvent.kt │ │ │ ├── relatedanime │ │ │ ├── DefaultRelatedAnimeHandler.kt │ │ │ ├── RelatedAnimeEvents.kt │ │ │ └── RelatedAnimeHandler.kt │ │ │ ├── search │ │ │ ├── DefaultSearchHandler.kt │ │ │ ├── FileSearchEvents.kt │ │ │ ├── SearchHandler.kt │ │ │ ├── SearchType.kt │ │ │ ├── anime │ │ │ │ ├── AnimeEntryFinishedEvent.kt │ │ │ │ ├── AnimeEntryFoundEvent.kt │ │ │ │ ├── AnimeSearchEntryFoundEvent.kt │ │ │ │ └── AnimeSearchFinishedEvent.kt │ │ │ ├── season │ │ │ │ ├── AnimeSeasonEntryFoundEvent.kt │ │ │ │ └── AnimeSeasonSearchFinishedEvent.kt │ │ │ └── similaranime │ │ │ │ ├── SimilarAnimeFoundEvent.kt │ │ │ │ └── SimilarAnimeSearchFinishedEvent.kt │ │ │ ├── state │ │ │ ├── InternalState.kt │ │ │ ├── State.kt │ │ │ └── snapshot │ │ │ │ ├── Snapshot.kt │ │ │ │ └── StateSnapshot.kt │ │ │ └── versioning │ │ │ ├── DefaultLatestVersionChecker.kt │ │ │ ├── GithubVersionProvider.kt │ │ │ ├── LatestVersionChecker.kt │ │ │ ├── NewVersionAvailableEvent.kt │ │ │ ├── ResourceBasedVersionProvider.kt │ │ │ ├── SemanticVersion.kt │ │ │ └── VersionProvider.kt │ └── resources │ │ ├── config │ │ └── animelist.dtd │ │ └── manami.version │ └── test │ ├── kotlin │ └── io │ │ └── github │ │ └── manamiproject │ │ └── manami │ │ └── app │ │ ├── cache │ │ ├── DefaultAnimeCacheTest.kt │ │ ├── TestingAssets.kt │ │ ├── loader │ │ │ ├── DependentCacheLoaderTest.kt │ │ │ └── SimpleCacheLoaderTest.kt │ │ └── populator │ │ │ ├── AnimeCachePopulatorTest.kt │ │ │ └── DeadEntriesCachePopulatorTest.kt │ │ ├── commands │ │ ├── GenericReversibleCommandTest.kt │ │ ├── TestingAssets.kt │ │ └── history │ │ │ ├── DefaultCommandHistoryTest.kt │ │ │ └── SimpleEventStreamTest.kt │ │ ├── events │ │ ├── EventfulListTest.kt │ │ ├── SimpleEventBusTest.kt │ │ └── TestingAssets.kt │ │ ├── extensions │ │ └── CollectionExtensionsKtTest.kt │ │ ├── file │ │ ├── CmdNewFileTest.kt │ │ ├── CmdOpenFileTest.kt │ │ ├── DefaultFileHandlerTest.kt │ │ ├── DefaultFileWriterTest.kt │ │ ├── FileParserTest.kt │ │ ├── ManamiVersionHandlerTest.kt │ │ └── TestingAssets.kt │ │ ├── inconsistencies │ │ ├── DefaultInconsistenciesHandlerTest.kt │ │ ├── TestingAssets.kt │ │ ├── animelist │ │ │ ├── deadentries │ │ │ │ └── AnimeListDeadEntriesInconsistenciesHandlerTest.kt │ │ │ ├── episodes │ │ │ │ └── AnimeListEpisodesInconsistenciesHandlerTest.kt │ │ │ └── metadata │ │ │ │ └── AnimeListMetaDataInconsistenciesHandlerTest.kt │ │ └── lists │ │ │ ├── deadentries │ │ │ ├── CmdFixDeadEntriesTest.kt │ │ │ └── DeadEntriesInconsistencyHandlerTest.kt │ │ │ └── metadata │ │ │ ├── CmdFixMetaDataTest.kt │ │ │ └── MetaDataInconsistencyHandlerTest.kt │ │ ├── lists │ │ ├── DefaultListHandlerTest.kt │ │ ├── LinkEntryKtTest.kt │ │ ├── animelist │ │ │ ├── AnimeListEntryTest.kt │ │ │ ├── CmdAddAnimeListEntryTest.kt │ │ │ ├── CmdRemoveAnimeListEntryTest.kt │ │ │ └── CmdReplaceAnimeListEntryTest.kt │ │ ├── ignorelist │ │ │ ├── CmdAddIgnoreListEntryTest.kt │ │ │ ├── CmdRemoveIgnoreListEntryTest.kt │ │ │ └── IgnoreListEntryTest.kt │ │ └── watchlist │ │ │ ├── CmdAddWatchListEntryTest.kt │ │ │ ├── CmdRemoveWatchListEntryTest.kt │ │ │ └── WatchListEntryTest.kt │ │ ├── migration │ │ ├── CmdMigrateEntriesTest.kt │ │ ├── CmdRemoveUnmappedMigrationEntriesTest.kt │ │ └── DefaultMetaDataMigrationHandlerTest.kt │ │ ├── relatedanime │ │ └── DefaultRelatedAnimeHandlerTest.kt │ │ ├── search │ │ ├── DefaultSearchHandlerTest.kt │ │ └── SearchTypeTest.kt │ │ ├── state │ │ ├── InternalStateTest.kt │ │ └── TestingAssets.kt │ │ └── versioning │ │ ├── DefaultLatestVersionCheckerTest.kt │ │ ├── GithubVersionProviderTest.kt │ │ ├── ResourceBasedVersionProviderTest.kt │ │ ├── SemanticVersionTest.kt │ │ └── TestingAssets.kt │ └── resources │ ├── cache_tests │ ├── loader │ │ ├── kitsu │ │ │ ├── 42194.json │ │ │ ├── 42194_relations.json │ │ │ └── 42194_tags.json │ │ └── notify │ │ │ ├── 3lack4eiR.json │ │ │ └── 3lack4eiR_relations.json │ └── populator │ │ └── test-database.json │ ├── file │ └── FileParser │ │ ├── animelist.dtd │ │ ├── correctly_parse_entries.xml │ │ ├── url_encoded_location.xml │ │ └── version_too_old.xml │ ├── fileimport │ └── parser │ │ └── manami │ │ └── LegacyManamiParser │ │ ├── animelist.dtd │ │ ├── convert_type_music_to_special.xml │ │ ├── correctly_parse_entries.xml │ │ └── version_too_new.xml │ ├── search_tests │ └── similar_anime_tests │ │ └── anime-offline-database-minified.json │ └── versioning_tests │ └── github_versioning_tests │ └── latest_version.json ├── manami-gui ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── manamiproject │ │ │ └── manami │ │ │ └── gui │ │ │ ├── BigPicturedAnimeEntry.kt │ │ │ ├── ImageViewCache.kt │ │ │ ├── ManamiAccess.kt │ │ │ ├── ReadOnlyObservableValue.kt │ │ │ ├── SafelyExecuteActionController.kt │ │ │ ├── Start.kt │ │ │ ├── animelist │ │ │ ├── AnimeForm.kt │ │ │ ├── AnimeFormTrigger.kt │ │ │ ├── AnimeListView.kt │ │ │ ├── ShowAnimeListTabRequest.kt │ │ │ ├── SimpleAnimeFormTriggerProperty.kt │ │ │ ├── SimpleAnimeListEntryProperty.kt │ │ │ └── SimpleAnimeProperty.kt │ │ │ ├── components │ │ │ ├── Alerts.kt │ │ │ ├── AnimeTable.kt │ │ │ ├── ApplicationBlockedLoading.kt │ │ │ ├── PathChooser.kt │ │ │ ├── SimpleAddEntry.kt │ │ │ ├── SimpleServiceStart.kt │ │ │ └── Tiles.kt │ │ │ ├── dashboard │ │ │ └── DashboardView.kt │ │ │ ├── events │ │ │ └── GuiEvents.kt │ │ │ ├── extensions │ │ │ ├── EventTargetExtensions.kt │ │ │ ├── NodeExtensions.kt │ │ │ └── TabPaneExtensions.kt │ │ │ ├── ignorelist │ │ │ ├── IgnoreListView.kt │ │ │ └── ShowIgnoreListTabRequest.kt │ │ │ ├── inconsistencies │ │ │ ├── DiffFragment.kt │ │ │ ├── InconsistenciesView.kt │ │ │ └── ShowInconsistenciesTabRequest.kt │ │ │ ├── main │ │ │ ├── MainWindowView.kt │ │ │ ├── MenuController.kt │ │ │ ├── MenuView.kt │ │ │ ├── QuitController.kt │ │ │ └── TabPaneView.kt │ │ │ ├── migration │ │ │ ├── MetaDataProviderMigrationView.kt │ │ │ ├── MigrationAlerts.kt │ │ │ ├── MigrationTableEntry.kt │ │ │ └── ShowMetaDataProviderMigrationViewTabRequest.kt │ │ │ ├── relatedanime │ │ │ ├── RelatedAnimeView.kt │ │ │ └── ShowRelatedAnimeTabRequest.kt │ │ │ ├── search │ │ │ ├── ClearAutoCompleteSuggestionsGuiEvent.kt │ │ │ ├── SearchBoxView.kt │ │ │ ├── anime │ │ │ │ ├── AnimeSearchView.kt │ │ │ │ └── ShowAnimeSearchTabRequest.kt │ │ │ ├── file │ │ │ │ ├── FileSearchView.kt │ │ │ │ └── ShowFileSearchTabRequest.kt │ │ │ ├── season │ │ │ │ ├── AnimeSeasonView.kt │ │ │ │ └── ShowAnimeSeasonTabRequest.kt │ │ │ └── similaranime │ │ │ │ ├── ShowSimilarAnimeSearchTabRequest.kt │ │ │ │ └── SimilarAnimeSearchView.kt │ │ │ └── watchlist │ │ │ ├── ShowWatchListTabRequest.kt │ │ │ └── WatchListView.kt │ └── resources │ │ └── .gitemptydir │ └── test │ ├── kotlin │ └── .gitemptydir │ └── resources │ └── .gitemptydir ├── renovate.json └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Problem 3 | about: You experienced a problem? 4 | title: '' 5 | labels: bug 6 | assignees: manami-project 7 | --- 8 | 9 | _Before posting: Please check if you have the newest version. If there is a newer version please check if the problem still occurs with the newest version._ 10 | 11 | **Manami version:** 12 | 13 | **operating system + version**: 14 | 15 | **JDK/JRE + version:** 16 | 17 | **Steps to reproduce:** 18 | 19 | **What is the current behavior:** 20 | 21 | **What is the expected behavior:** -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze (${{ matrix.language }}) 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 25 16 | 17 | permissions: 18 | security-events: write 19 | packages: read 20 | actions: read 21 | contents: read 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | include: 27 | - language: kotlin 28 | build-mode: manual 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: ${{ matrix.language }} 38 | build-mode: ${{ matrix.build-mode }} 39 | 40 | - name: Build project 41 | if: matrix.build-mode == 'manual' 42 | env: 43 | GH_PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} 44 | shell: bash 45 | run: ./gradlew compileKotlin 46 | 47 | - name: Perform CodeQL analysis 48 | uses: github/codeql-action/analyze@v3 49 | with: 50 | category: /language:${{ matrix.language }} -------------------------------------------------------------------------------- /.github/workflows/housekeeping.yml: -------------------------------------------------------------------------------- 1 | name: 'Housekeeping' 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | housekeeping: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Workflow housekeeping 15 | uses: Mattraks/delete-workflow-runs@v2 16 | with: 17 | retain_days: 30 18 | keep_minimum_runs: 1 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Extract release version 17 | run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV 18 | - name: Set up JDK 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'zulu' 22 | java-version: 21 23 | - name: Set executable flag on gradlew 24 | run: chmod +x gradlew 25 | - name: Execute tests 26 | env: 27 | GH_PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} 28 | run: ./gradlew test -Prelease.version=$RELEASE_VERSION 29 | - name: Set version 30 | run: echo $RELEASE_VERSION > manami-app/src/main/resources/manami.version 31 | - name: Build executable fatJar 32 | env: 33 | GH_PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} 34 | run: ./gradlew shadowJar -Prelease.version=$RELEASE_VERSION 35 | - name: Upload file to release 36 | uses: softprops/action-gh-release@v2 37 | with: 38 | files: manami-gui/build/libs/manami.jar 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | - id: fetch-latest-release 42 | name: Fetch latest release tag 43 | run: | 44 | version=$(git tag --sort=creatordate | tail -2 | head -1) 45 | echo "PREVIOUS_VERSION=$version" >> $GITHUB_OUTPUT 46 | - name: Delete outdated release assets 47 | uses: mknejp/delete-release-assets@v1 48 | with: 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | tag: ${{ steps.fetch-latest-release.outputs.PREVIOUS_VERSION }} 51 | assets: manami.jar 52 | fail-if-no-release: false 53 | fail-if-no-assets: false -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests' 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | branches: 9 | - '**' 10 | merge_group: 11 | branches: 12 | - 'master' 13 | paths-ignore: 14 | - 'README.md' 15 | - '.gitignore' 16 | - '.gitattributes' 17 | - '.github/workflows/**' 18 | - '!.github/workflows/tests.yml' 19 | 20 | jobs: 21 | tests: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | java: [ 21 ] 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Set up JDK ${{ matrix.java }} 31 | uses: actions/setup-java@v4 32 | with: 33 | distribution: 'zulu' 34 | java-version: ${{ matrix.java }} 35 | - name: Set executable flag on gradlew 36 | run: chmod +x gradlew 37 | - name: Execute tests 38 | env: 39 | GH_PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} 40 | run: ./gradlew test 41 | - name: Publish Test Results 42 | uses: EnricoMi/publish-unit-test-result-action@v2 43 | if: github.ref != 'refs/heads/master' && always() 44 | with: 45 | check_name: Test results JDK ${{ matrix.java }} 46 | comment_title: Test results JDK ${{ matrix.java }} 47 | files: | 48 | /github/workspace/**/build/test-results/**/*.xml 49 | - name: Generate coverage 50 | if: ${{ matrix.java == 21 }} 51 | run: ./gradlew koverXmlReportJvm 52 | - name: Upload coverage 53 | uses: codecov/codecov-action@v5.4.3 54 | if: ${{ matrix.java == 21 }} 55 | with: 56 | token: ${{ secrets.CODECOV_TOKEN }} 57 | files: manami-app/build/reports/kover/reportJvm.xml 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests](https://github.com/manami-project/manami/actions/workflows/tests.yml/badge.svg)](https://github.com/manami-project/manami/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/manami-project/manami/graph/badge.svg?token=DkoslLUvTn)](https://codecov.io/gh/manami-project/manami) ![jdk21](https://img.shields.io/badge/jdk-21-informational) 2 | # Manami 3 | 4 | ## What does it do? 5 | Manami creates an index file for the anime that you already watched and stored on your hard drive. Additionally based on this list the tool can assist you in finding more anime that you might enjoy. 6 | 7 | ## All features at a glance 8 | * Categorize anime in watched, plan to watch/watching, ignore 9 | * Automatically fill out anime data upon adding a new entry 10 | * supports multiple meta data providers using data from [anime-offline-database](https://github.com/manami-project/anime-offline-database) 11 | * Find anime by tags 12 | * Browse anime seasons 13 | * Find related anime for all entries in your anime list 14 | * Find similar anime based on tags 15 | * Suggestions to ignore anime 16 | * Migrate entries from one meta data provider to another 17 | * Find inconsistencies 18 | * Find updated meta data in animelist/watchlist/ignorelist entries and automatically fix them 19 | * Find dead entries in animelist 20 | * Find dead entries in watchlist/ignorelist and automatically remove them 21 | * Check if number of files equals number of expected episodes 22 | * No lock-in. The data is persisted as XML file and therefore can be converted and migrated very easily 23 | * No need to provide username or password 24 | * No installer, it's portable 25 | 26 | ## What it doesn't do 27 | * The tool won't let you update your profile on sites like myanimelist.net, kistu.io,... 28 | 29 | ## Why? 30 | * I created this tool for my personal needs to support my workflow. 31 | 32 | ## Installation 33 | * Requires JDK 21 and the possibility to run JavaFX 34 | * Download the *.jar file of the latest release 35 | * No installation or additional setup needed. Just Download the `*.jar` and start it by double click or via console `java -jar manami.jar`. 36 | 37 | ## Configuration 38 | 39 | See ["Configuration Management"](https://github.com/manami-project/modb-core/tree/master#configuration-management) 40 | 41 | | parameter | type | default | description | 42 | |------------------------------|-----------|---------|--------------------------------------------------------------------------------------------------------------| 43 | | `manami.cache.useLocalFiles` | `Boolean` | `true` | Downloads anime-offline-database files once and stores them next to the *.jar file. Redownload after 24 hrs. | 44 | 45 | ## Dataset 46 | 47 | Uses data from [https://github.com/manami-project/anime-offline-database](https://github.com/manami-project/anime-offline-database), which is made available 48 | here under the [Open Database License (ODbL)](https://opendatacommons.org/licenses/odbl/1-0/). -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Resolves "The Kotlin Gradle plugin was loaded multiple times" message 2 | plugins { 3 | alias(libs.plugins.kotlin.jvm) apply false 4 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manami-project/manami/c3686157de21911b3795fba89bbc357d5b0eae3c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /manami-app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | alias(libs.plugins.kover) 6 | `java-library` 7 | } 8 | 9 | val githubUsername = "manami-project" 10 | 11 | repositories { 12 | mavenCentral() 13 | maven { 14 | name = "modb-app" 15 | url = uri("https://maven.pkg.github.com/$githubUsername/modb-app") 16 | credentials { 17 | username = parameter("GH_USERNAME", githubUsername) 18 | password = parameter("GH_PACKAGES_READ_TOKEN") 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation(libs.bundles.kotlin) 25 | implementation(libs.bundles.modb) 26 | implementation(libs.logback.classic) 27 | implementation(libs.bundles.apache.commons) 28 | 29 | testImplementation(libs.modb.test) 30 | } 31 | 32 | kotlin { 33 | jvmToolchain(JavaVersion.VERSION_21.toString().toInt()) 34 | } 35 | 36 | tasks.withType().configureEach { 37 | compilerOptions { 38 | jvmTarget.set(JvmTarget.JVM_21) 39 | apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) 40 | languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) 41 | } 42 | } 43 | 44 | tasks.withType { 45 | useJUnitPlatform() 46 | reports.html.required.set(false) 47 | reports.junitXml.required.set(true) 48 | maxParallelForks = rootProject.extra["maxParallelForks"] as Int 49 | } 50 | 51 | fun parameter(name: String, default: String = ""): String { 52 | val env = System.getenv(name) ?: "" 53 | if (env.isNotBlank()) { 54 | return env 55 | } 56 | 57 | val property = project.findProperty(name) as String? ?: "" 58 | if (property.isNotEmpty()) { 59 | return property 60 | } 61 | 62 | return default 63 | } 64 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/ManamiApp.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app 2 | 3 | import io.github.manamiproject.manami.app.file.FileHandler 4 | import io.github.manamiproject.manami.app.inconsistencies.InconsistenciesHandler 5 | import io.github.manamiproject.manami.app.lists.ListHandler 6 | import io.github.manamiproject.manami.app.migration.MetaDataMigrationHandler 7 | import io.github.manamiproject.manami.app.relatedanime.RelatedAnimeHandler 8 | import io.github.manamiproject.manami.app.search.SearchHandler 9 | 10 | interface ManamiApp: SearchHandler, FileHandler, ListHandler, RelatedAnimeHandler, InconsistenciesHandler, MetaDataMigrationHandler { 11 | 12 | fun quit(ignoreUnsavedChanged: Boolean = false) 13 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/AnimeCache.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache 2 | 3 | import io.github.manamiproject.modb.core.config.Hostname 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | import io.github.manamiproject.modb.core.anime.Tag 6 | import java.net.URI 7 | 8 | interface AnimeCache : Cache> { 9 | 10 | val availableMetaDataProvider: Set 11 | val availableTags: Set 12 | 13 | fun allEntries(metaDataProvider: Hostname): Sequence 14 | 15 | fun mapToMetaDataProvider(uri: URI, metaDataProvider: Hostname): Set 16 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/Cache.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache 2 | 3 | interface Cache> { 4 | 5 | fun fetch(key: KEY): VALUE 6 | 7 | fun populate(key: KEY, value: VALUE) 8 | 9 | fun clear() 10 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/CacheEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache 2 | 3 | sealed class CacheEntry 4 | class DeadEntry: CacheEntry() 5 | class PresentValue(val value: T) : CacheEntry() -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/loader/CacheLoader.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.loader 2 | 3 | import io.github.manamiproject.modb.core.config.Hostname 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | import java.net.URI 6 | 7 | internal interface CacheLoader { 8 | fun hostname(): Hostname 9 | fun loadAnime(uri: URI): Anime 10 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/loader/DependentCacheLoader.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.loader 2 | 3 | import io.github.manamiproject.modb.core.config.AnimeId 4 | import io.github.manamiproject.modb.core.config.Hostname 5 | import io.github.manamiproject.modb.core.config.MetaDataProviderConfig 6 | import io.github.manamiproject.modb.core.converter.AnimeConverter 7 | import io.github.manamiproject.modb.core.downloader.Downloader 8 | import io.github.manamiproject.modb.core.extensions.writeToFile 9 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 10 | import io.github.manamiproject.modb.core.anime.Anime 11 | import io.github.manamiproject.modb.core.anime.AnimeRawToAnimeTransformer 12 | import io.github.manamiproject.modb.core.anime.DefaultAnimeRawToAnimeTransformer 13 | import kotlinx.coroutines.runBlocking 14 | import java.net.URI 15 | import java.nio.file.Path 16 | import kotlin.io.path.deleteIfExists 17 | 18 | internal class DependentCacheLoader( 19 | private val config: MetaDataProviderConfig, 20 | private val animeDownloader: Downloader, 21 | private val relationsDownloader: Downloader, 22 | private val relationsDir: Path, 23 | private val converter: AnimeConverter, 24 | private val transformer: AnimeRawToAnimeTransformer = DefaultAnimeRawToAnimeTransformer.instance, 25 | ) : CacheLoader { 26 | 27 | override fun loadAnime(uri: URI): Anime { 28 | log.debug { "Loading anime from [$uri]" } 29 | 30 | val id = config.extractAnimeId(uri) 31 | 32 | loadRelations(id) 33 | 34 | val result = runBlocking { animeDownloader.download(id) } 35 | val anime = runBlocking { converter.convert(result) } 36 | 37 | relationsDir.resolve("$id.${config.fileSuffix()}").deleteIfExists() 38 | 39 | return transformer.transform(anime) 40 | } 41 | 42 | override fun hostname(): Hostname = config.hostname() 43 | 44 | private fun loadRelations(id: AnimeId) = runBlocking { 45 | relationsDownloader.download(id).writeToFile(relationsDir.resolve("$id.${config.fileSuffix()}")) 46 | } 47 | 48 | private companion object { 49 | private val log by LoggerDelegate() 50 | } 51 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/loader/SimpleCacheLoader.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.loader 2 | 3 | import io.github.manamiproject.modb.core.config.Hostname 4 | import io.github.manamiproject.modb.core.config.MetaDataProviderConfig 5 | import io.github.manamiproject.modb.core.converter.AnimeConverter 6 | import io.github.manamiproject.modb.core.downloader.Downloader 7 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 8 | import io.github.manamiproject.modb.core.anime.Anime 9 | import io.github.manamiproject.modb.core.anime.AnimeRawToAnimeTransformer 10 | import io.github.manamiproject.modb.core.anime.DefaultAnimeRawToAnimeTransformer 11 | import kotlinx.coroutines.runBlocking 12 | import java.net.URI 13 | 14 | internal class SimpleCacheLoader( 15 | private val config: MetaDataProviderConfig, 16 | private val downloader: Downloader, 17 | private val converter: AnimeConverter, 18 | private val transformer: AnimeRawToAnimeTransformer = DefaultAnimeRawToAnimeTransformer.instance, 19 | ) : CacheLoader { 20 | 21 | override fun hostname(): Hostname = config.hostname() 22 | 23 | override fun loadAnime(uri: URI): Anime { 24 | log.debug { "Loading anime from [$uri]" } 25 | 26 | val id = config.extractAnimeId(uri) 27 | val rawContent = runBlocking { downloader.download(id) } 28 | return runBlocking { transformer.transform(converter.convert(rawContent)) } 29 | } 30 | 31 | private companion object { 32 | private val log by LoggerDelegate() 33 | } 34 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/populator/CachePopulator.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.populator 2 | 3 | import io.github.manamiproject.manami.app.cache.Cache 4 | import io.github.manamiproject.manami.app.cache.CacheEntry 5 | 6 | internal interface CachePopulator> { 7 | suspend fun populate(cache: Cache) 8 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/populator/CachePopulatorFinishedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.populator 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | object CachePopulatorFinishedEvent: Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/cache/populator/NumberOfEntriesPerMetaDataProviderEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.populator 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.modb.core.config.Hostname 5 | 6 | data class NumberOfEntriesPerMetaDataProviderEvent(val entries: Map): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/Command.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands 2 | 3 | internal interface Command { 4 | 5 | /** 6 | * @return **true** if could be executed successfully 7 | */ 8 | fun execute(): Boolean 9 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/GenericReversibleCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands 2 | 3 | import io.github.manamiproject.manami.app.commands.history.CommandHistory 4 | import io.github.manamiproject.manami.app.commands.history.DefaultCommandHistory 5 | import io.github.manamiproject.manami.app.state.InternalState 6 | import io.github.manamiproject.manami.app.state.State 7 | import io.github.manamiproject.manami.app.state.snapshot.Snapshot 8 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 9 | 10 | internal class GenericReversibleCommand( 11 | private val state: State = InternalState, 12 | private val commandHistory: CommandHistory = DefaultCommandHistory, 13 | private val command: Command, 14 | ) : ReversibleCommand { 15 | 16 | private var snapshot: StatefulSnapshot = Uninitialized 17 | private var hasBeenPushedToCommandHistory: Boolean = false 18 | 19 | override fun undo() { 20 | when(snapshot) { 21 | Uninitialized -> throw IllegalStateException("Cannot undo command which hasn't been executed") 22 | is Initialized -> state.restore((snapshot as Initialized).snapshot) 23 | } 24 | } 25 | 26 | override fun execute(): Boolean { 27 | val unsavedSnapshot = Initialized(state.createSnapshot()) 28 | 29 | val successfullyExecuted = command.execute() 30 | 31 | if (!successfullyExecuted) { 32 | log.warn { "Command wasn't executed successfully." } 33 | return successfullyExecuted 34 | } 35 | 36 | snapshot = unsavedSnapshot 37 | 38 | if (!hasBeenPushedToCommandHistory) { 39 | commandHistory.push(this) 40 | hasBeenPushedToCommandHistory = true 41 | } 42 | 43 | return successfullyExecuted 44 | } 45 | 46 | companion object { 47 | private val log by LoggerDelegate() 48 | } 49 | } 50 | 51 | private sealed class StatefulSnapshot 52 | private data object Uninitialized : StatefulSnapshot() 53 | private data class Initialized(val snapshot: Snapshot) : StatefulSnapshot() -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/ReversibleCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands 2 | 3 | internal interface ReversibleCommand : Command { 4 | 5 | fun undo() 6 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/history/CommandHistory.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands.history 2 | 3 | import io.github.manamiproject.manami.app.commands.ReversibleCommand 4 | 5 | internal interface CommandHistory { 6 | 7 | fun push(command: ReversibleCommand) 8 | 9 | fun isUndoPossible(): Boolean 10 | 11 | fun undo() 12 | 13 | fun isRedoPossible(): Boolean 14 | 15 | fun redo() 16 | 17 | fun isSaved(): Boolean 18 | 19 | fun isUnsaved(): Boolean 20 | 21 | fun save() 22 | 23 | fun clear() 24 | } 25 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/history/DefaultCommandHistory.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands.history 2 | 3 | import io.github.manamiproject.manami.app.commands.ReversibleCommand 4 | import io.github.manamiproject.manami.app.events.SimpleEventBus 5 | 6 | internal object DefaultCommandHistory : CommandHistory { 7 | 8 | private val commandHistory: EventStream = SimpleEventStream() 9 | private var lastSavedCommand: ReversibleCommand = NoOpCommand 10 | 11 | override fun push(command: ReversibleCommand) { 12 | commandHistory.add(command) 13 | SimpleEventBus.post(FileSavedStatusChangedEvent(isSaved())) 14 | SimpleEventBus.post(UndoRedoStatusEvent(isUndoPossible(), isRedoPossible())) 15 | } 16 | 17 | override fun isUndoPossible(): Boolean = commandHistory.hasPrevious() 18 | 19 | override fun undo() { 20 | if (isUndoPossible()) { 21 | commandHistory.element().undo() 22 | commandHistory.previous() 23 | SimpleEventBus.post(FileSavedStatusChangedEvent(isSaved())) 24 | SimpleEventBus.post(UndoRedoStatusEvent(isUndoPossible(), isRedoPossible())) 25 | } 26 | } 27 | 28 | override fun isRedoPossible(): Boolean = commandHistory.hasNext() 29 | 30 | override fun redo() { 31 | if (isRedoPossible()) { 32 | commandHistory.next() 33 | commandHistory.element().execute() 34 | SimpleEventBus.post(FileSavedStatusChangedEvent(isSaved())) 35 | SimpleEventBus.post(UndoRedoStatusEvent(isUndoPossible(), isRedoPossible())) 36 | } 37 | } 38 | 39 | override fun isSaved(): Boolean = !isUnsaved() 40 | 41 | override fun isUnsaved(): Boolean { 42 | val isStreamEmpty = !commandHistory.hasPrevious() && !commandHistory.hasNext() 43 | val isFirstElement = !commandHistory.hasPrevious() && commandHistory.hasNext() 44 | 45 | return if (isStreamEmpty || isFirstElement) { 46 | false 47 | } else { 48 | commandHistory.element() != lastSavedCommand 49 | } 50 | } 51 | 52 | override fun save() { 53 | if (isUnsaved()) { 54 | lastSavedCommand = if (commandHistory.hasPrevious()) { 55 | commandHistory.element() 56 | } else { 57 | NoOpCommand 58 | } 59 | 60 | commandHistory.crop() 61 | SimpleEventBus.post(FileSavedStatusChangedEvent(isSaved())) 62 | SimpleEventBus.post(UndoRedoStatusEvent(isUndoPossible(), isRedoPossible())) 63 | } 64 | } 65 | 66 | override fun clear() { 67 | commandHistory.clear() 68 | SimpleEventBus.post(FileSavedStatusChangedEvent(isSaved())) 69 | SimpleEventBus.post(UndoRedoStatusEvent(isUndoPossible(), isRedoPossible())) 70 | } 71 | } 72 | 73 | private object NoOpCommand : ReversibleCommand { 74 | override fun undo() {} 75 | override fun execute() = true 76 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/history/EventStream.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands.history 2 | 3 | internal interface EventStream { 4 | 5 | fun add(element: T) 6 | 7 | fun element(): T 8 | 9 | fun hasPrevious(): Boolean 10 | 11 | fun previous(): Boolean 12 | 13 | fun hasNext(): Boolean 14 | 15 | fun next(): Boolean 16 | 17 | fun crop() 18 | 19 | fun clear() 20 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/history/FileSavedStatusChangedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands.history 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class FileSavedStatusChangedEvent(val isFileSaved: Boolean) : Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/history/SimpleEventStream.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands.history 2 | 3 | internal class SimpleEventStream : EventStream { 4 | 5 | private var cursorIndex: Int = 0 6 | private val elements: MutableList = mutableListOf(InitialState) 7 | 8 | override fun add(element: T) { 9 | val position = cursorIndex + 1 10 | elements.add(position, Event(element)) 11 | cursorIndex++ 12 | 13 | val positionOfLastElement = elements.size - 1 14 | 15 | if (cursorIndex != positionOfLastElement) { 16 | for (index in positionOfLastElement downTo cursorIndex + 1) { 17 | elements.removeAt(index) 18 | } 19 | } 20 | } 21 | 22 | override fun element(): T { 23 | @Suppress("UNCHECKED_CAST") 24 | return when(val element = elements[cursorIndex]) { 25 | InitialState -> throw IllegalStateException("Cannot retrieve element from empty EventStream.") 26 | is Event<*> -> element.element as T 27 | } 28 | } 29 | 30 | override fun hasPrevious(): Boolean = cursorIndex > 0 31 | 32 | override fun previous(): Boolean { 33 | return if (cursorIndex == 0) { 34 | false 35 | } else { 36 | cursorIndex-- 37 | return true 38 | } 39 | } 40 | 41 | override fun hasNext(): Boolean = cursorIndex < elements.size - 1 42 | 43 | override fun next(): Boolean { 44 | return if (elements.isEmpty() || cursorIndex == elements.size - 1) { 45 | false 46 | } else { 47 | cursorIndex++ 48 | return true 49 | } 50 | } 51 | 52 | override fun crop() { 53 | while (hasNext()) { 54 | elements.removeAt(cursorIndex + 1) 55 | } 56 | } 57 | 58 | override fun clear() { 59 | elements.clear() 60 | elements.add(InitialState) 61 | cursorIndex = 0 62 | } 63 | } 64 | 65 | private sealed class Events 66 | private data object InitialState : Events() 67 | private data class Event(val element: T) : Events() -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/commands/history/UndoRedoStatusEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands.history 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class UndoRedoStatusEvent( 6 | val isUndoPossible: Boolean, 7 | val isRedoPossible: Boolean, 8 | ): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/events/Event.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.events 2 | 3 | interface Event 4 | 5 | enum class EventListType { 6 | ANIME_LIST, 7 | WATCH_LIST, 8 | IGNORE_LIST; 9 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/events/EventBus.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.events 2 | 3 | interface EventBus { 4 | 5 | fun subscribe(subscriber: Any) 6 | 7 | fun unsubscribe(subscriber: Any) 8 | 9 | fun post(event: Event) 10 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/events/Subscribe.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.events 2 | 3 | import kotlin.annotation.AnnotationRetention.RUNTIME 4 | import kotlin.annotation.AnnotationTarget.FUNCTION 5 | import kotlin.reflect.KClass 6 | 7 | @Target(FUNCTION) 8 | @Retention(RUNTIME) 9 | internal annotation class Subscribe(vararg val types: KClass) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/extensions/CollectionExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.extensions 2 | 3 | inline fun Collection<*>.castToSet(): Set { 4 | require(this.all { it!!::class == T::class }) { "Not all items are of type [${T::class}]" } 5 | return this.asSequence().map { it as T }.toSet() 6 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/CmdNewFile.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.commands.history.CommandHistory 5 | import io.github.manamiproject.manami.app.commands.history.DefaultCommandHistory 6 | import io.github.manamiproject.manami.app.state.InternalState 7 | import io.github.manamiproject.manami.app.state.State 8 | 9 | internal class CmdNewFile( 10 | private val state: State = InternalState, 11 | private val commandHistory: CommandHistory = DefaultCommandHistory, 12 | ) : Command { 13 | 14 | override fun execute(): Boolean { 15 | commandHistory.clear() 16 | state.closeFile() 17 | state.clear() 18 | return true 19 | } 20 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/CmdOpenFile.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.commands.history.CommandHistory 5 | import io.github.manamiproject.manami.app.commands.history.DefaultCommandHistory 6 | import io.github.manamiproject.manami.app.state.InternalState 7 | import io.github.manamiproject.manami.app.state.State 8 | import io.github.manamiproject.modb.core.extensions.RegularFile 9 | 10 | internal class CmdOpenFile( 11 | private val state: State = InternalState, 12 | private val commandHistory: CommandHistory = DefaultCommandHistory, 13 | private val file: RegularFile, 14 | private val parsedFile: ParsedManamiFile, 15 | ) : Command { 16 | 17 | override fun execute(): Boolean { 18 | commandHistory.clear() 19 | state.clear() 20 | state.closeFile() 21 | state.addAllAnimeListEntries(parsedFile.animeListEntries) 22 | state.addAllWatchListEntries(parsedFile.watchListEntries) 23 | state.addAllIgnoreListEntries(parsedFile.ignoreListEntries) 24 | state.setOpenedFile(file) 25 | return true 26 | } 27 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/DefaultFileHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.commands.history.CommandHistory 4 | import io.github.manamiproject.manami.app.commands.history.DefaultCommandHistory 5 | import io.github.manamiproject.manami.app.events.EventBus 6 | import io.github.manamiproject.manami.app.events.SimpleEventBus 7 | import io.github.manamiproject.manami.app.state.CurrentFile 8 | import io.github.manamiproject.manami.app.state.InternalState 9 | import io.github.manamiproject.manami.app.state.State 10 | import io.github.manamiproject.modb.core.extensions.RegularFile 11 | import io.github.manamiproject.modb.core.extensions.regularFileExists 12 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 13 | import kotlin.io.path.createFile 14 | 15 | internal class DefaultFileHandler( 16 | private val state: State = InternalState, 17 | private val commandHistory: CommandHistory = DefaultCommandHistory, 18 | private val parser: Parser = FileParser(), 19 | private val fileWriter: FileWriter = DefaultFileWriter(), 20 | private val eventBus: EventBus = SimpleEventBus, 21 | ) : FileHandler { 22 | 23 | override fun newFile(ignoreUnsavedChanged: Boolean) { 24 | log.info { "Creating new file using [ignoreUnsavedChanged=$ignoreUnsavedChanged]" } 25 | 26 | if (!ignoreUnsavedChanged) { 27 | check(commandHistory.isSaved()) { "Cannot create a new list, because there are unsaved changes." } 28 | } 29 | 30 | CmdNewFile( 31 | state = state, 32 | commandHistory = commandHistory, 33 | ).execute() 34 | } 35 | 36 | override fun open(file: RegularFile, ignoreUnsavedChanged: Boolean) { 37 | log.info { "Opening file [$file] using [ignoreUnsavedChanged=$ignoreUnsavedChanged]" } 38 | 39 | if (!ignoreUnsavedChanged) { 40 | check(commandHistory.isSaved()) { "Cannot open file, because there are unsaved changes." } 41 | } 42 | 43 | val parsedFile = parser.parse(file) 44 | 45 | CmdOpenFile( 46 | state = state, 47 | commandHistory = commandHistory, 48 | parsedFile = parsedFile, 49 | file = file, 50 | ).execute() 51 | eventBus.post(FileOpenedEvent(file.fileName.toString())) 52 | } 53 | 54 | override fun isOpenFileSet(): Boolean = state.openedFile() is CurrentFile 55 | 56 | override fun isSaved(): Boolean = commandHistory.isSaved() 57 | 58 | override fun isUnsaved(): Boolean = commandHistory.isUnsaved() 59 | 60 | override fun save() { 61 | if (commandHistory.isSaved()) { 62 | return 63 | } 64 | 65 | val file = state.openedFile() 66 | check(file is CurrentFile) { "No file set" } 67 | 68 | fileWriter.writeTo(file.regularFile) 69 | commandHistory.save() 70 | } 71 | 72 | override fun saveAs(file: RegularFile) { 73 | if (!file.regularFileExists()) { 74 | file.createFile() 75 | } 76 | 77 | state.setOpenedFile(file) 78 | eventBus.post(SavedAsFileEvent(file.fileName.toString())) 79 | save() 80 | } 81 | 82 | override fun isUndoPossible(): Boolean = commandHistory.isUndoPossible() 83 | 84 | override fun undo() = commandHistory.undo() 85 | 86 | override fun isRedoPossible(): Boolean = commandHistory.isRedoPossible() 87 | 88 | override fun redo() = commandHistory.redo() 89 | 90 | private companion object { 91 | private val log by LoggerDelegate() 92 | } 93 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/FileHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.modb.core.extensions.RegularFile 4 | 5 | interface FileHandler { 6 | 7 | fun newFile(ignoreUnsavedChanged: Boolean = false) 8 | fun open(file: RegularFile, ignoreUnsavedChanged: Boolean = false) 9 | fun isOpenFileSet(): Boolean 10 | 11 | fun isSaved(): Boolean 12 | fun isUnsaved(): Boolean 13 | fun save() 14 | fun saveAs(file: RegularFile) 15 | 16 | fun isUndoPossible(): Boolean 17 | fun undo() 18 | 19 | fun isRedoPossible(): Boolean 20 | fun redo() 21 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/FileOpenedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class FileOpenedEvent(val fileName: String): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/FileWriter.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.modb.core.extensions.RegularFile 4 | 5 | interface FileWriter { 6 | 7 | fun writeTo(file: RegularFile) 8 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/ManamiVersionHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.versioning.SemanticVersion 4 | import io.github.manamiproject.modb.core.extensions.EMPTY 5 | import org.xml.sax.Attributes 6 | import org.xml.sax.EntityResolver 7 | import org.xml.sax.InputSource 8 | import org.xml.sax.helpers.DefaultHandler 9 | 10 | internal class ManamiVersionHandler : DefaultHandler() { 11 | 12 | private var strBuilder = StringBuilder() 13 | 14 | private var _version = SemanticVersion() 15 | val version 16 | get() = _version 17 | 18 | var entityResolver: EntityResolver = EntityResolver { _, _ -> InputSource(EMPTY) } 19 | 20 | override fun resolveEntity(publicId: String?, systemId: String?): InputSource = entityResolver.resolveEntity(publicId, systemId) 21 | 22 | override fun characters(ch: CharArray, start: Int, length: Int) { 23 | strBuilder.append(String(ch, start, length)) 24 | } 25 | 26 | override fun startElement(namespaceUri: String, localName: String, qName: String, attributes: Attributes) { 27 | 28 | when (qName) { 29 | "manami" -> { 30 | strBuilder = StringBuilder() 31 | strBuilder.append(attributes.getValue("version").trim()) 32 | } 33 | } 34 | } 35 | 36 | override fun endDocument() { 37 | _version = SemanticVersion(strBuilder.toString()) 38 | } 39 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/ParsedManamiFile.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | 7 | internal data class ParsedManamiFile( 8 | val animeListEntries: Set = emptySet(), 9 | val watchListEntries: Set = emptySet(), 10 | val ignoreListEntries: Set = emptySet(), 11 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/Parser.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.modb.core.config.FileSuffix 4 | import io.github.manamiproject.modb.core.extensions.RegularFile 5 | 6 | internal interface Parser { 7 | 8 | fun parse(file: RegularFile): T 9 | 10 | /** 11 | * @return The supported file suffix without a dot. **Example** _xml_ 12 | */ 13 | fun handlesSuffix(): FileSuffix 14 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/file/SavedAsFileEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class SavedAsFileEvent(val fileName: String): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/InconsistenciesCheckFinishedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | object InconsistenciesCheckFinishedEvent: Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/InconsistenciesHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies 2 | 3 | import io.github.manamiproject.manami.app.inconsistencies.animelist.metadata.AnimeListMetaDataDiff 4 | 5 | interface InconsistenciesHandler { 6 | 7 | fun fixAnimeListEntryMetaDataInconsistencies(diff: AnimeListMetaDataDiff) 8 | 9 | fun findInconsistencies(config: InconsistenciesSearchConfig) 10 | 11 | fun fixMetaDataInconsistencies() 12 | 13 | fun fixDeadEntryInconsistencies() 14 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/InconsistenciesProgressEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class InconsistenciesProgressEvent( 6 | val finishedTasks: Int, 7 | val numberOfTasks: Int, 8 | ): Event 9 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/InconsistenciesSearchConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies 2 | 3 | data class InconsistenciesSearchConfig( 4 | val checkAnimeListMetaData: Boolean = false, 5 | val checkAnimeListDeadEnties: Boolean = false, 6 | val checkAnimeListEpisodes: Boolean = false, 7 | val checkMetaData: Boolean = true, 8 | val checkDeadEntries: Boolean = true, 9 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/InconsistencyHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies 2 | 3 | internal interface InconsistencyHandler { 4 | 5 | fun isExecutable(config: InconsistenciesSearchConfig): Boolean 6 | 7 | fun calculateWorkload(): Int 8 | 9 | fun execute(progressUpdate: (Int) -> Unit = {}): RESULT 10 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/deadentries/AnimeListDeadEntriesInconsistenciesHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.deadentries 2 | 3 | import io.github.manamiproject.manami.app.cache.Cache 4 | import io.github.manamiproject.manami.app.cache.CacheEntry 5 | import io.github.manamiproject.manami.app.cache.DeadEntry 6 | import io.github.manamiproject.manami.app.cache.DefaultAnimeCache 7 | import io.github.manamiproject.manami.app.inconsistencies.InconsistenciesSearchConfig 8 | import io.github.manamiproject.manami.app.inconsistencies.InconsistencyHandler 9 | import io.github.manamiproject.manami.app.lists.Link 10 | import io.github.manamiproject.manami.app.state.InternalState 11 | import io.github.manamiproject.manami.app.state.State 12 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 13 | import io.github.manamiproject.modb.core.anime.Anime 14 | import java.net.URI 15 | 16 | internal class AnimeListDeadEntriesInconsistenciesHandler( 17 | private val state: State = InternalState, 18 | private val cache: Cache> = DefaultAnimeCache.instance, 19 | ) : InconsistencyHandler { 20 | 21 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = config.checkAnimeListDeadEnties 22 | 23 | override fun calculateWorkload(): Int = state.animeList().count { it.link is Link } 24 | 25 | override fun execute(progressUpdate: (Int) -> Unit): AnimeListDeadEntriesInconsistenciesResult { 26 | log.info { "Starting check for dead entries in AnimeList." } 27 | 28 | var progress = 0 29 | 30 | val result = state.animeList() 31 | .asSequence() 32 | .filter { it.link is Link } 33 | .map { 34 | progressUpdate.invoke(++progress) 35 | it 36 | } 37 | .map { it to cache.fetch(it.link.asLink().uri) } 38 | .filter { it.second is DeadEntry } 39 | .map { it.first } 40 | .toList() 41 | 42 | log.info { "Finished check for dead entries in AnimeList." } 43 | 44 | return AnimeListDeadEntriesInconsistenciesResult( 45 | entries = result, 46 | ) 47 | } 48 | 49 | companion object { 50 | private val log by LoggerDelegate() 51 | } 52 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/deadentries/AnimeListDeadEntriesInconsistenciesResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.deadentries 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | 5 | data class AnimeListDeadEntriesInconsistenciesResult( 6 | val entries: List, 7 | ) 8 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/deadentries/AnimeListDeadEntriesInconsistenciesResultEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.deadentries 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 5 | 6 | data class AnimeListDeadEntriesInconsistenciesResultEvent( 7 | val entries: List, 8 | ): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/episodes/AnimeListEpisodesInconsistenciesHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.episodes 2 | 3 | import io.github.manamiproject.manami.app.inconsistencies.InconsistenciesSearchConfig 4 | import io.github.manamiproject.manami.app.inconsistencies.InconsistencyHandler 5 | import io.github.manamiproject.manami.app.lists.Link 6 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 7 | import io.github.manamiproject.manami.app.state.CurrentFile 8 | import io.github.manamiproject.manami.app.state.InternalState 9 | import io.github.manamiproject.manami.app.state.State 10 | import io.github.manamiproject.modb.core.extensions.regularFileExists 11 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 12 | import kotlin.io.path.listDirectoryEntries 13 | 14 | internal class AnimeListEpisodesInconsistenciesHandler( 15 | private val state: State = InternalState, 16 | ) : InconsistencyHandler { 17 | 18 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = config.checkAnimeListEpisodes 19 | 20 | override fun calculateWorkload(): Int = state.animeList().count { it.link is Link } 21 | 22 | override fun execute(progressUpdate: (Int) -> Unit): AnimeListEpisodesInconsistenciesResult { 23 | log.info { "Starting check for differing episodes in AnimeList." } 24 | 25 | var progress = 0 26 | 27 | val results = state.animeList() 28 | .asSequence() 29 | .filter { it.link is Link } 30 | .map { 31 | progressUpdate.invoke(++progress) 32 | it 33 | } 34 | .map { it to fetchNumberOfEpisodes(it) } 35 | .filter { it.first.episodes != it.second } 36 | .map { 37 | EpisodeDiff( 38 | animeListEntry = it.first, 39 | numberOfFiles = it.second, 40 | ) 41 | } 42 | .toList() 43 | 44 | log.info { "Finished check for differing episodes in AnimeList." } 45 | 46 | return AnimeListEpisodesInconsistenciesResult(entries = results) 47 | } 48 | 49 | private fun fetchNumberOfEpisodes(entry: AnimeListEntry): Int { 50 | val folder = (state.openedFile() as CurrentFile).regularFile.parent.resolve(entry.location.toString()) 51 | 52 | return folder.listDirectoryEntries() 53 | .asSequence() 54 | .filter { it.regularFileExists() } 55 | .filterNot { it.fileName.toString().startsWith('.') } 56 | .count() 57 | } 58 | 59 | companion object { 60 | private val log by LoggerDelegate() 61 | } 62 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/episodes/AnimeListEpisodesInconsistenciesResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.episodes 2 | 3 | data class AnimeListEpisodesInconsistenciesResult( 4 | val entries: List, 5 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/episodes/AnimeListEpisodesInconsistenciesResultEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.episodes 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class AnimeListEpisodesInconsistenciesResultEvent( 6 | val entries: List, 7 | ) : Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/episodes/EpisodeDiff.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.episodes 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.modb.core.anime.Episodes 5 | 6 | data class EpisodeDiff( 7 | val animeListEntry: AnimeListEntry, 8 | val numberOfFiles: Episodes, 9 | ) 10 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/metadata/AnimeListMetaDataInconsistenciesHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.metadata 2 | 3 | import io.github.manamiproject.manami.app.cache.Cache 4 | import io.github.manamiproject.manami.app.cache.CacheEntry 5 | import io.github.manamiproject.manami.app.cache.DefaultAnimeCache 6 | import io.github.manamiproject.manami.app.cache.PresentValue 7 | import io.github.manamiproject.manami.app.inconsistencies.InconsistenciesSearchConfig 8 | import io.github.manamiproject.manami.app.inconsistencies.InconsistencyHandler 9 | import io.github.manamiproject.manami.app.lists.Link 10 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 11 | import io.github.manamiproject.manami.app.state.InternalState 12 | import io.github.manamiproject.manami.app.state.State 13 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 14 | import io.github.manamiproject.modb.core.anime.Anime 15 | import java.net.URI 16 | 17 | 18 | internal class AnimeListMetaDataInconsistenciesHandler( 19 | private val state: State = InternalState, 20 | private val cache: Cache> = DefaultAnimeCache.instance, 21 | ): InconsistencyHandler { 22 | 23 | override fun calculateWorkload(): Int = state.animeList().count { it.link is Link } 24 | 25 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = config.checkAnimeListMetaData 26 | 27 | override fun execute(progressUpdate: (Int) -> Unit): AnimeListMetaDataInconsistenciesResult { 28 | log.info { "Starting check for meta data inconsistencies in AnimeList." } 29 | 30 | var progress = 0 31 | 32 | val result = state.animeList() 33 | .asSequence() 34 | .filter { it.link is Link } 35 | .map { 36 | progressUpdate.invoke(++progress) 37 | it 38 | } 39 | .map { it to cache.fetch(it.link.asLink().uri) } 40 | .filter { it.second is PresentValue } 41 | .map { toAnimeListEntry(currentEntry = it.first, anime = (it.second as PresentValue).value) } 42 | .filterNot { it.first == it.second } 43 | .map { AnimeListMetaDataDiff(currentEntry = it.first, replacementEntry = it.second) } 44 | .toList() 45 | 46 | log.info { "Finished check for meta data inconsistencies in AnimeList." } 47 | 48 | return AnimeListMetaDataInconsistenciesResult( 49 | entries = result, 50 | ) 51 | } 52 | 53 | private fun toAnimeListEntry(currentEntry: AnimeListEntry, anime: Anime): Pair { 54 | return currentEntry to currentEntry.copy( 55 | title = anime.title, 56 | thumbnail = anime.thumbnail, 57 | episodes = anime.episodes, 58 | type = anime.type, 59 | ) 60 | } 61 | 62 | companion object { 63 | private val log by LoggerDelegate() 64 | } 65 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/metadata/AnimeListMetaDataInconsistenciesResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.metadata 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | 5 | internal data class AnimeListMetaDataInconsistenciesResult( 6 | val entries: List = emptyList(), 7 | ) 8 | 9 | data class AnimeListMetaDataDiff( 10 | val currentEntry: AnimeListEntry, 11 | val replacementEntry: AnimeListEntry, 12 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/animelist/metadata/AnimeListMetaDataInconsistenciesResultEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.animelist.metadata 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class AnimeListMetaDataInconsistenciesResultEvent( 6 | val diff: AnimeListMetaDataDiff, 7 | ) : Event 8 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/deadentries/CmdFixDeadEntries.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.deadentries 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | import io.github.manamiproject.manami.app.state.State 7 | 8 | internal class CmdFixDeadEntries( 9 | private val state: State, 10 | private val removeWatchList: List = emptyList(), 11 | private val removeIgnoreList: List = emptyList(), 12 | ) : Command { 13 | 14 | override fun execute(): Boolean { 15 | removeWatchList.forEach { 16 | state.removeWatchListEntry(it) 17 | } 18 | 19 | removeIgnoreList.forEach { 20 | state.removeIgnoreListEntry(it) 21 | } 22 | 23 | return true 24 | } 25 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/deadentries/DeadEntriesInconsistenciesResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.deadentries 2 | 3 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 4 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 5 | 6 | internal data class DeadEntriesInconsistenciesResult( 7 | val watchListResults: List = emptyList(), 8 | val ignoreListResults: List = emptyList(), 9 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/deadentries/DeadEntriesInconsistenciesResultEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.deadentries 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class DeadEntriesInconsistenciesResultEvent(val numberOfAffectedEntries: Int): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/deadentries/DeadEntriesInconsistencyHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.deadentries 2 | 3 | import io.github.manamiproject.manami.app.cache.Cache 4 | import io.github.manamiproject.manami.app.cache.CacheEntry 5 | import io.github.manamiproject.manami.app.cache.DeadEntry 6 | import io.github.manamiproject.manami.app.cache.DefaultAnimeCache 7 | import io.github.manamiproject.manami.app.inconsistencies.InconsistenciesSearchConfig 8 | import io.github.manamiproject.manami.app.inconsistencies.InconsistencyHandler 9 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 10 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 11 | import io.github.manamiproject.manami.app.state.InternalState 12 | import io.github.manamiproject.manami.app.state.State 13 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 14 | import io.github.manamiproject.modb.core.anime.Anime 15 | import java.net.URI 16 | 17 | internal class DeadEntriesInconsistencyHandler( 18 | private val state: State = InternalState, 19 | private val cache: Cache> = DefaultAnimeCache.instance, 20 | ): InconsistencyHandler { 21 | 22 | override fun calculateWorkload(): Int = state.watchList().size + state.ignoreList().size 23 | 24 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = config.checkDeadEntries 25 | 26 | override fun execute(progressUpdate: (Int) -> Unit): DeadEntriesInconsistenciesResult { 27 | log.info { "Starting check for dead entries in WatchList and IgnoreList." } 28 | 29 | var progress = 0 30 | 31 | val watchListResults: List = state.watchList() 32 | .asSequence() 33 | .map { 34 | progressUpdate.invoke(++progress) 35 | it 36 | } 37 | .map { watchListEntry -> watchListEntry to cache.fetch(watchListEntry.link.uri) } 38 | .filter { it.second is DeadEntry } 39 | .map { it.first } 40 | .toList() 41 | 42 | val ignoreListResults: List = state.ignoreList() 43 | .asSequence() 44 | .map { 45 | progressUpdate.invoke(++progress) 46 | it 47 | } 48 | .map { ignoreListEntry -> ignoreListEntry to cache.fetch(ignoreListEntry.link.uri) } 49 | .filter { it.second is DeadEntry } 50 | .map { it.first } 51 | .toList() 52 | 53 | log.info { "Finished check for dead entries in WatchList and IgnoreList." } 54 | 55 | return DeadEntriesInconsistenciesResult( 56 | watchListResults = watchListResults, 57 | ignoreListResults = ignoreListResults, 58 | ) 59 | } 60 | 61 | companion object { 62 | private val log by LoggerDelegate() 63 | } 64 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/metadata/CmdFixMetaData.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.metadata 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | import io.github.manamiproject.manami.app.state.State 7 | 8 | internal class CmdFixMetaData( 9 | private val state: State, 10 | private val diffWatchList: List> = emptyList(), 11 | private val diffIgnoreList: List> = emptyList(), 12 | ): Command { 13 | 14 | override fun execute(): Boolean { 15 | diffWatchList.forEach { 16 | state.removeWatchListEntry(it.currentEntry) 17 | } 18 | if (diffWatchList.isNotEmpty()) { 19 | state.addAllWatchListEntries(diffWatchList.map { it.newEntry }) 20 | } 21 | 22 | diffIgnoreList.forEach { 23 | state.removeIgnoreListEntry(it.currentEntry) 24 | } 25 | if (diffIgnoreList.isNotEmpty()) { 26 | state.addAllIgnoreListEntries(diffIgnoreList.map { it.newEntry }) 27 | } 28 | 29 | return true 30 | } 31 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/metadata/MetaDataDiff.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.metadata 2 | 3 | data class MetaDataDiff( 4 | val currentEntry: T, 5 | val newEntry: T, 6 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/metadata/MetaDataInconsistenciesResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.metadata 2 | 3 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 4 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 5 | 6 | internal data class MetaDataInconsistenciesResult( 7 | val watchListResults: List> = emptyList(), 8 | val ignoreListResults: List> = emptyList(), 9 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/metadata/MetaDataInconsistenciesResultEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.metadata 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class MetaDataInconsistenciesResultEvent(val numberOfAffectedEntries: Int): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/inconsistencies/lists/metadata/MetaDataInconsistencyHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies.lists.metadata 2 | 3 | import io.github.manamiproject.manami.app.cache.Cache 4 | import io.github.manamiproject.manami.app.cache.CacheEntry 5 | import io.github.manamiproject.manami.app.cache.DefaultAnimeCache 6 | import io.github.manamiproject.manami.app.cache.PresentValue 7 | import io.github.manamiproject.manami.app.inconsistencies.InconsistenciesSearchConfig 8 | import io.github.manamiproject.manami.app.inconsistencies.InconsistencyHandler 9 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 10 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 11 | import io.github.manamiproject.manami.app.state.InternalState 12 | import io.github.manamiproject.manami.app.state.State 13 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 14 | import io.github.manamiproject.modb.core.anime.Anime 15 | import java.net.URI 16 | 17 | internal class MetaDataInconsistencyHandler( 18 | private val state: State = InternalState, 19 | private val cache: Cache> = DefaultAnimeCache.instance, 20 | ) : InconsistencyHandler { 21 | 22 | override fun calculateWorkload(): Int = state.watchList().size + state.ignoreList().size 23 | 24 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = config.checkMetaData 25 | 26 | override fun execute(progressUpdate: (Int) -> Unit): MetaDataInconsistenciesResult { 27 | log.info { "Starting check for meta data inconsistencies in WatchList and IgnoreList." } 28 | 29 | var progress = 0 30 | 31 | val watchListResults: List> = state.watchList() 32 | .asSequence() 33 | .map { 34 | progressUpdate.invoke(++progress) 35 | it 36 | } 37 | .map { watchListEntry -> watchListEntry to cache.fetch(watchListEntry.link.uri) } 38 | .filter { it.second is PresentValue } 39 | .map { it.first to (it.second as PresentValue).value } 40 | .map { it.first to WatchListEntry(it.second) } 41 | .filter { it.first.link == it.second.link } 42 | .filter { it.first.title != it.second.title || it.first.thumbnail != it.second.thumbnail } 43 | .map { MetaDataDiff(currentEntry = it.first, newEntry = it.second) } 44 | .toList() 45 | 46 | val ignoreListResults: List> = state.ignoreList() 47 | .asSequence() 48 | .map { 49 | progressUpdate.invoke(++progress) 50 | it 51 | } 52 | .map { ignoreListEntry -> ignoreListEntry to cache.fetch(ignoreListEntry.link.uri) } 53 | .filter { it.second is PresentValue } 54 | .map { it.first to (it.second as PresentValue).value } 55 | .map { it.first to IgnoreListEntry(it.second) } 56 | .filter { it.first.link == it.second.link } 57 | .filter { it.first != it.second } 58 | .map { MetaDataDiff(currentEntry = it.first, newEntry = it.second) } 59 | .toList() 60 | 61 | log.info { "Finished check for meta data inconsistencies in WatchList and IgnoreList." } 62 | 63 | return MetaDataInconsistenciesResult( 64 | watchListResults = watchListResults, 65 | ignoreListResults = ignoreListResults, 66 | ) 67 | } 68 | 69 | companion object { 70 | private val log by LoggerDelegate() 71 | } 72 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/AnimeEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists 2 | 3 | import io.github.manamiproject.modb.core.anime.Title 4 | import java.net.URI 5 | 6 | interface AnimeEntry { 7 | val link: LinkEntry 8 | val title: Title 9 | val thumbnail: URI 10 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/LinkEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists 2 | 3 | import io.github.manamiproject.modb.core.extensions.EMPTY 4 | import java.net.URI 5 | 6 | sealed class LinkEntry { 7 | fun asLink(): Link = this as Link 8 | } 9 | 10 | object NoLink: LinkEntry() { 11 | override fun toString(): String = EMPTY 12 | } 13 | 14 | data class Link(val uri: URI): LinkEntry() { 15 | 16 | constructor(uri: String): this( 17 | uri = URI(uri) 18 | ) 19 | 20 | override fun toString(): String = uri.toString() 21 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/ListChangedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.manami.app.events.EventListType 5 | 6 | data class ListChangedEvent( 7 | val list: EventListType, 8 | val type: EventType, 9 | val obj: Set, 10 | ) : Event { 11 | 12 | enum class EventType { 13 | ADDED, 14 | REMOVED, 15 | } 16 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/ListHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | import java.net.URI 7 | 8 | interface ListHandler { 9 | 10 | fun addAnimeListEntry(entry: AnimeListEntry) 11 | fun animeList(): List 12 | fun removeAnimeListEntry(entry: AnimeListEntry) 13 | fun replaceAnimeListEntry(current: AnimeListEntry, replacement: AnimeListEntry) 14 | 15 | fun addWatchListEntry(uris: Collection) 16 | fun watchList(): Set 17 | fun removeWatchListEntry(entry: WatchListEntry) 18 | 19 | fun addIgnoreListEntry(uris: Collection) 20 | fun ignoreList(): Set 21 | fun removeIgnoreListEntry(entry: IgnoreListEntry) 22 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/animelist/AnimeListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.animelist 2 | 3 | import io.github.manamiproject.manami.app.lists.AnimeEntry 4 | import io.github.manamiproject.manami.app.lists.LinkEntry 5 | import io.github.manamiproject.manami.app.lists.NoLink 6 | import io.github.manamiproject.manami.app.state.CurrentFile 7 | import io.github.manamiproject.manami.app.state.OpenedFile 8 | import io.github.manamiproject.modb.core.anime.* 9 | import io.github.manamiproject.modb.core.anime.AnimeMedia.NO_PICTURE_THUMBNAIL 10 | import io.github.manamiproject.modb.core.extensions.directoryExists 11 | import java.net.URI 12 | import java.nio.file.Path 13 | import kotlin.io.path.Path 14 | 15 | data class AnimeListEntry( 16 | override val link: LinkEntry = NoLink, 17 | override val title: Title, 18 | override val thumbnail: URI = NO_PICTURE_THUMBNAIL, 19 | val episodes: Episodes, 20 | val type: AnimeType, 21 | val location: Path, 22 | ): AnimeEntry { 23 | 24 | init { 25 | validateLocation() 26 | } 27 | 28 | internal fun convertLocationToRelativePath(openedFile: OpenedFile): AnimeListEntry { 29 | val locationString = if (location.toString().startsWith("/")) "/$location" else location.toString() 30 | var location = Path(locationString) 31 | 32 | if (openedFile is CurrentFile) { 33 | val startDir = openedFile.regularFile.parent 34 | 35 | when (startDir == location) { 36 | true -> { 37 | location = Path(".") 38 | } 39 | false -> { 40 | location = startDir.resolve(location) 41 | location = startDir.relativize(location) 42 | } 43 | } 44 | } 45 | 46 | return copy(location = location) 47 | } 48 | 49 | private fun validateLocation() { 50 | if (location.toString().startsWith("/")) { 51 | require(location.directoryExists()) { "Location is not a directory or does not exist." } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/animelist/CmdAddAnimeListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.animelist 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.state.State 5 | 6 | internal class CmdAddAnimeListEntry( 7 | private val state: State, 8 | private val animeListEntry: AnimeListEntry, 9 | ): Command { 10 | 11 | override fun execute(): Boolean { 12 | if (state.animeListEntrtyExists(animeListEntry)) { 13 | return false 14 | } 15 | 16 | state.addAllAnimeListEntries(setOf(animeListEntry.convertLocationToRelativePath(state.openedFile()))) 17 | 18 | return true 19 | } 20 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/animelist/CmdRemoveAnimeListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.animelist 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.state.InternalState 5 | import io.github.manamiproject.manami.app.state.State 6 | 7 | internal class CmdRemoveAnimeListEntry( 8 | private val state: State = InternalState, 9 | private val animeListEntry: AnimeListEntry, 10 | ): Command { 11 | 12 | override fun execute(): Boolean { 13 | if (!state.animeListEntrtyExists(animeListEntry)) { 14 | return false 15 | } 16 | 17 | state.removeAnimeListEntry(animeListEntry) 18 | 19 | return true 20 | } 21 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/animelist/CmdReplaceAnimeListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.animelist 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.state.InternalState 5 | import io.github.manamiproject.manami.app.state.State 6 | 7 | internal class CmdReplaceAnimeListEntry( 8 | private val state: State = InternalState, 9 | private val currentEntry: AnimeListEntry, 10 | private val replacementEntry: AnimeListEntry, 11 | ): Command { 12 | 13 | override fun execute(): Boolean { 14 | if (!state.animeListEntrtyExists(currentEntry)) { 15 | return false 16 | } 17 | 18 | state.removeAnimeListEntry(currentEntry) 19 | state.addAllAnimeListEntries(setOf(replacementEntry.convertLocationToRelativePath(state.openedFile()))) 20 | 21 | return true 22 | } 23 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/ignorelist/AddIgnoreListStatusUpdateEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.ignorelist 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class AddIgnoreListStatusUpdateEvent(val finishedTasks: Int, val tasks: Int): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/ignorelist/CmdAddIgnoreListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.ignorelist 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.state.InternalState 5 | import io.github.manamiproject.manami.app.state.State 6 | 7 | internal class CmdAddIgnoreListEntry( 8 | private val state: State = InternalState, 9 | private val ignoreListEntry: IgnoreListEntry, 10 | ): Command { 11 | 12 | override fun execute(): Boolean { 13 | if (state.ignoreList().map { it.link }.any { it == ignoreListEntry.link }) { 14 | return false 15 | } 16 | 17 | state.addAllIgnoreListEntries(setOf(ignoreListEntry)) 18 | return true 19 | } 20 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/ignorelist/CmdRemoveIgnoreListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.ignorelist 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.state.InternalState 5 | import io.github.manamiproject.manami.app.state.State 6 | 7 | internal class CmdRemoveIgnoreListEntry( 8 | val state: State = InternalState, 9 | val ignoreListEntry: IgnoreListEntry, 10 | ): Command { 11 | 12 | override fun execute(): Boolean { 13 | if (state.ignoreList().map { it.link }.none { it == ignoreListEntry.link }) { 14 | return false 15 | } 16 | 17 | state.removeIgnoreListEntry(ignoreListEntry) 18 | return true 19 | } 20 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/ignorelist/IgnoreListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.ignorelist 2 | 3 | import io.github.manamiproject.manami.app.lists.AnimeEntry 4 | import io.github.manamiproject.manami.app.lists.Link 5 | import io.github.manamiproject.modb.core.anime.Anime 6 | import io.github.manamiproject.modb.core.anime.Title 7 | import java.net.URI 8 | 9 | data class IgnoreListEntry( 10 | override val link: Link, 11 | override val title: Title, 12 | override val thumbnail: URI, 13 | ) : AnimeEntry { 14 | 15 | constructor(anime: Anime): this( 16 | link = Link(anime.sources.first()), 17 | title = anime.title, 18 | thumbnail = anime.thumbnail, 19 | ) 20 | 21 | constructor(animeEntry: AnimeEntry): this( 22 | link = animeEntry.link.asLink(), 23 | title = animeEntry.title, 24 | thumbnail = animeEntry.thumbnail, 25 | ) 26 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/watchlist/AddWatchListStatusUpdateEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.watchlist 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class AddWatchListStatusUpdateEvent(val finishedTasks: Int, val tasks: Int): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/watchlist/CmdAddWatchListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.watchlist 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.state.InternalState 5 | import io.github.manamiproject.manami.app.state.State 6 | 7 | internal class CmdAddWatchListEntry( 8 | private val state: State = InternalState, 9 | private val watchListEntry: WatchListEntry, 10 | ): Command { 11 | 12 | override fun execute(): Boolean { 13 | if (state.watchList().map { it.link }.any { it == watchListEntry.link }) { 14 | return false 15 | } 16 | 17 | state.addAllWatchListEntries(setOf(watchListEntry)) 18 | return true 19 | } 20 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/watchlist/CmdRemoveWatchListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.watchlist 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.state.InternalState 5 | import io.github.manamiproject.manami.app.state.State 6 | 7 | internal class CmdRemoveWatchListEntry( 8 | val state: State = InternalState, 9 | val watchListEntry: WatchListEntry, 10 | ) : Command { 11 | 12 | override fun execute(): Boolean { 13 | if (state.watchList().map { it.link }.none { it == watchListEntry.link }) { 14 | return false 15 | } 16 | 17 | state.removeWatchListEntry(watchListEntry) 18 | return true 19 | } 20 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/lists/watchlist/WatchListEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.watchlist 2 | 3 | import io.github.manamiproject.manami.app.lists.AnimeEntry 4 | import io.github.manamiproject.manami.app.lists.Link 5 | import io.github.manamiproject.modb.core.anime.Anime 6 | import io.github.manamiproject.modb.core.anime.AnimeStatus 7 | import io.github.manamiproject.modb.core.anime.AnimeStatus.UNKNOWN 8 | import io.github.manamiproject.modb.core.anime.Title 9 | import java.net.URI 10 | 11 | data class WatchListEntry( 12 | override val link: Link, 13 | override val title: Title, 14 | override val thumbnail: URI, 15 | val status: AnimeStatus = UNKNOWN, 16 | ) : AnimeEntry { 17 | 18 | constructor(anime: Anime): this( 19 | link = Link(anime.sources.first()), 20 | title = anime.title, 21 | thumbnail = anime.thumbnail, 22 | ) 23 | 24 | constructor(animeEntry: AnimeEntry): this( 25 | link = animeEntry.link.asLink(), 26 | title = animeEntry.title, 27 | thumbnail = animeEntry.thumbnail, 28 | ) 29 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/migration/CmdMigrateEntries.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.migration 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.lists.Link 5 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 6 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 7 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 8 | import io.github.manamiproject.manami.app.state.InternalState 9 | import io.github.manamiproject.manami.app.state.State 10 | 11 | internal class CmdMigrateEntries( 12 | private val state: State = InternalState, 13 | private val animeListMappings: Map, 14 | private val watchListMappings: Map, 15 | private val ignoreListMappings: Map, 16 | ): Command { 17 | 18 | override fun execute(): Boolean { 19 | if (animeListMappings.isNotEmpty()) { 20 | state.addAllAnimeListEntries( 21 | animeListMappings.map { 22 | it.key.copy(link = it.value) 23 | } 24 | ) 25 | 26 | animeListMappings.forEach { 27 | state.removeAnimeListEntry(it.key) 28 | } 29 | } 30 | 31 | if (watchListMappings.isNotEmpty()) { 32 | state.addAllWatchListEntries( 33 | watchListMappings.map { 34 | it.key.copy(link = it.value) 35 | } 36 | ) 37 | 38 | watchListMappings.forEach { 39 | state.removeWatchListEntry(it.key) 40 | } 41 | } 42 | 43 | if (ignoreListMappings.isNotEmpty()) { 44 | state.addAllIgnoreListEntries( 45 | ignoreListMappings.map { 46 | it.key.copy(link = it.value) 47 | } 48 | ) 49 | 50 | ignoreListMappings.forEach { 51 | state.removeIgnoreListEntry(it.key) 52 | } 53 | } 54 | 55 | return true 56 | } 57 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/migration/CmdRemoveUnmappedMigrationEntries.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.migration 2 | 3 | import io.github.manamiproject.manami.app.commands.Command 4 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 5 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 6 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 7 | import io.github.manamiproject.manami.app.state.InternalState 8 | import io.github.manamiproject.manami.app.state.State 9 | 10 | internal class CmdRemoveUnmappedMigrationEntries( 11 | private val state: State = InternalState, 12 | private val animeListEntriesWithoutMapping: Collection, 13 | private val watchListEntriesWithoutMapping: Collection, 14 | private val ignoreListEntriesWithoutMapping: Collection, 15 | ): Command { 16 | 17 | override fun execute(): Boolean { 18 | animeListEntriesWithoutMapping.forEach { 19 | state.removeAnimeListEntry(it) 20 | } 21 | 22 | watchListEntriesWithoutMapping.forEach { 23 | state.removeWatchListEntry(it) 24 | } 25 | 26 | ignoreListEntriesWithoutMapping.forEach { 27 | state.removeIgnoreListEntry(it) 28 | } 29 | 30 | return true 31 | } 32 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/migration/MetaDataMigrationHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.migration 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 5 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 6 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 7 | import io.github.manamiproject.modb.core.config.Hostname 8 | 9 | interface MetaDataMigrationHandler { 10 | 11 | fun checkMigration(metaDataProviderFrom: Hostname, metaDataProviderTo: Hostname) 12 | 13 | fun migrate( 14 | animeListMappings: Map = emptyMap(), 15 | watchListMappings: Map = emptyMap(), 16 | ignoreListMappings: Map = emptyMap(), 17 | ) 18 | 19 | fun removeUnmapped( 20 | animeListEntriesWithoutMapping: Collection, 21 | watchListEntriesWithoutMapping: Collection, 22 | ignoreListEntriesWithoutMapping: Collection, 23 | ) 24 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/migration/MetaDataMigrationProgressEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.migration 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class MetaDataMigrationProgressEvent( 6 | val finishedTasks: Int, 7 | val numberOfTasks: Int, 8 | ): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/migration/MetaDataMigrationResultEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.migration 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.manami.app.lists.Link 5 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 6 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 7 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 8 | 9 | data class MetaDataMigrationResultEvent( 10 | val animeListEntriesWithoutMapping: Collection, 11 | val animeListEntiresMultipleMappings: Map>, 12 | val animeListMappings: Map, 13 | val watchListEntriesWithoutMapping: Collection, 14 | val watchListEntiresMultipleMappings: Map>, 15 | val watchListMappings: Map, 16 | val ignoreListEntriesWithoutMapping: Collection, 17 | val ignoreListEntiresMultipleMappings: Map>, 18 | val ignoreListMappings: Map, 19 | ) : Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/relatedanime/DefaultRelatedAnimeHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.relatedanime 2 | 3 | import io.github.manamiproject.manami.app.cache.AnimeCache 4 | import io.github.manamiproject.manami.app.cache.DefaultAnimeCache 5 | import io.github.manamiproject.manami.app.cache.PresentValue 6 | import io.github.manamiproject.manami.app.events.EventBus 7 | import io.github.manamiproject.manami.app.events.EventListType 8 | import io.github.manamiproject.manami.app.events.EventListType.ANIME_LIST 9 | import io.github.manamiproject.manami.app.events.EventListType.IGNORE_LIST 10 | import io.github.manamiproject.manami.app.events.SimpleEventBus 11 | import io.github.manamiproject.manami.app.lists.Link 12 | import io.github.manamiproject.manami.app.state.InternalState 13 | import io.github.manamiproject.manami.app.state.State 14 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 15 | import io.github.manamiproject.modb.core.anime.Anime 16 | import java.net.URI 17 | 18 | internal class DefaultRelatedAnimeHandler( 19 | private val cache: AnimeCache = DefaultAnimeCache.instance, 20 | private val state: State = InternalState, 21 | private val eventBus: EventBus = SimpleEventBus, 22 | ) : RelatedAnimeHandler { 23 | 24 | override fun findRelatedAnimeForAnimeList() { 25 | log.info { "Searching related anime for anime list." } 26 | findRelatedAnime(ANIME_LIST, state.animeList().map { it.link }.filterIsInstance().map { it.uri }.toSet()) 27 | } 28 | 29 | override fun findRelatedAnimeForIgnoreList() { 30 | log.info { "Searching related anime for ignore list." } 31 | findRelatedAnime(IGNORE_LIST, state.ignoreList().map { it.link.uri }.toSet()) 32 | } 33 | 34 | private fun findRelatedAnime(eventListType: EventListType, initialSources: Collection) { 35 | val entriesToCheck = HashSet() 36 | var lastSize = 0 37 | 38 | val initialRelatedAnime = initialSources.map { cache.fetch(it) } 39 | .filterIsInstance>() 40 | .flatMap { it.value.relatedAnime } 41 | initialSources.union(initialRelatedAnime).forEach { entriesToCheck.add(it) } 42 | 43 | val animeList = state.animeList().map { it.link }.filterIsInstance().map { it.uri }.toSet() 44 | val watchList = state.watchList().map { it.link.uri }.toSet() 45 | val ignoreList = state.ignoreList().map { it.link.uri }.toSet() 46 | 47 | log.info { "Initializing search for [$eventListType] related anime is done." } 48 | 49 | while (entriesToCheck.size != lastSize) { 50 | lastSize = entriesToCheck.size 51 | entriesToCheck.map { cache.fetch(it) } 52 | .filterIsInstance>() 53 | .flatMap { it.value.relatedAnime } 54 | .forEach { entriesToCheck.add(it) } 55 | } 56 | 57 | entriesToCheck.removeAll(animeList) 58 | entriesToCheck.removeAll(watchList) 59 | entriesToCheck.removeAll(ignoreList) 60 | 61 | eventBus.post(RelatedAnimeFinishedEvent(eventListType, entriesToCheck.map { cache.fetch(it) }.filterIsInstance>().map { it.value })) 62 | log.info { "Finished searching for [$eventListType] related anime" } 63 | } 64 | 65 | companion object { 66 | private val log by LoggerDelegate() 67 | } 68 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/relatedanime/RelatedAnimeEvents.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.relatedanime 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.manami.app.events.EventListType 5 | import io.github.manamiproject.modb.core.anime.Anime 6 | 7 | data class RelatedAnimeFinishedEvent(val listType: EventListType, val resultList: List): Event 8 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/relatedanime/RelatedAnimeHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.relatedanime 2 | 3 | interface RelatedAnimeHandler { 4 | 5 | fun findRelatedAnimeForAnimeList() 6 | fun findRelatedAnimeForIgnoreList() 7 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/FileSearchEvents.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 5 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 6 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 7 | 8 | data class FileSearchAnimeListResultsEvent(val anime: Collection): Event 9 | data class FileSearchWatchListResultsEvent(val anime: Collection): Event 10 | data class FileSearchIgnoreListResultsEvent(val anime: Collection): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/SearchHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search 2 | 3 | import io.github.manamiproject.manami.app.search.SearchType.AND 4 | import io.github.manamiproject.modb.core.config.Hostname 5 | import io.github.manamiproject.modb.core.anime.Anime 6 | import io.github.manamiproject.modb.core.anime.AnimeSeason 7 | import io.github.manamiproject.modb.core.anime.AnimeStatus 8 | import io.github.manamiproject.modb.core.anime.Tag 9 | import java.net.URI 10 | 11 | interface SearchHandler { 12 | 13 | fun findInLists(searchString: String) 14 | 15 | fun findSeason(season: AnimeSeason, metaDataProvider: Hostname) 16 | 17 | fun findByTag(tags: Set, metaDataProvider: Hostname, searchType: SearchType = AND, status: Set = AnimeStatus.entries.toSet()) 18 | 19 | fun findSimilarAnime(uri: URI) 20 | 21 | fun find(uri: URI) 22 | 23 | fun availableMetaDataProviders(): Set 24 | 25 | fun availableTags(): Set 26 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/SearchType.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search 2 | 3 | enum class SearchType { 4 | OR, 5 | AND; 6 | 7 | companion object { 8 | fun of(value: String): SearchType { 9 | return entries.find { it.toString().equals(value, ignoreCase = true) } ?: throw IllegalArgumentException("No value for [$value]") 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/anime/AnimeEntryFinishedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.anime 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | object AnimeEntryFinishedEvent: Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/anime/AnimeEntryFoundEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.anime 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | 6 | data class AnimeEntryFoundEvent(val anime: Anime): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/anime/AnimeSearchEntryFoundEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.anime 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | 6 | data class AnimeSearchEntryFoundEvent(val anime: Anime): Event 7 | -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/anime/AnimeSearchFinishedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.anime 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | object AnimeSearchFinishedEvent: Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/season/AnimeSeasonEntryFoundEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.season 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | 6 | data class AnimeSeasonEntryFoundEvent(val anime: Anime): Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/season/AnimeSeasonSearchFinishedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.season 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | object AnimeSeasonSearchFinishedEvent: Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/similaranime/SimilarAnimeFoundEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.similaranime 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | 6 | data class SimilarAnimeFoundEvent(val entries: List) : Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/search/similaranime/SimilarAnimeSearchFinishedEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search.similaranime 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | object SimilarAnimeSearchFinishedEvent : Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/state/State.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.state 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | import io.github.manamiproject.manami.app.state.snapshot.Snapshot 7 | import io.github.manamiproject.modb.core.extensions.RegularFile 8 | 9 | internal interface State { 10 | 11 | fun setOpenedFile(file: RegularFile) 12 | fun openedFile(): OpenedFile 13 | fun closeFile() 14 | 15 | fun animeListEntrtyExists(anime: AnimeListEntry): Boolean 16 | fun animeList(): List 17 | fun addAllAnimeListEntries(anime: Collection) 18 | fun removeAnimeListEntry(entry: AnimeListEntry) 19 | 20 | fun watchList(): Set 21 | fun addAllWatchListEntries(anime: Collection) 22 | fun removeWatchListEntry(entry: WatchListEntry) 23 | 24 | fun ignoreList(): Set 25 | fun addAllIgnoreListEntries(anime: Collection) 26 | fun removeIgnoreListEntry(entry: IgnoreListEntry) 27 | 28 | fun createSnapshot(): Snapshot 29 | fun restore(snapshot: Snapshot) 30 | 31 | fun clear() 32 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/state/snapshot/Snapshot.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.state.snapshot 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | 7 | internal interface Snapshot { 8 | 9 | fun animeList(): List 10 | 11 | fun watchList(): Set 12 | 13 | fun ignoreList(): Set 14 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/state/snapshot/StateSnapshot.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.state.snapshot 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | 7 | internal data class StateSnapshot( 8 | private val animeList: List = emptyList(), 9 | private val watchList: Set = emptySet(), 10 | private val ignoreList: Set = emptySet(), 11 | ) : Snapshot { 12 | 13 | override fun animeList(): List = animeList 14 | 15 | override fun watchList(): Set = watchList 16 | 17 | override fun ignoreList(): Set = ignoreList 18 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/versioning/DefaultLatestVersionChecker.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import io.github.manamiproject.manami.app.events.EventBus 4 | import io.github.manamiproject.manami.app.events.SimpleEventBus 5 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 6 | 7 | internal class DefaultLatestVersionChecker( 8 | private val currentVersionProvider: VersionProvider = ResourceBasedVersionProvider, 9 | private val latestVersionProvider: VersionProvider = GithubVersionProvider(), 10 | private val eventBus: EventBus = SimpleEventBus, 11 | ) : LatestVersionChecker { 12 | 13 | override fun checkLatestVersion() { 14 | log.info { "Checking if there is a new version available." } 15 | val currentVersion = currentVersionProvider.version() 16 | val latestVersion = latestVersionProvider.version() 17 | 18 | if (latestVersion.isNewerThan(currentVersion)) { 19 | log.info { "Found new version [$latestVersion]" } 20 | eventBus.post(NewVersionAvailableEvent(latestVersion)) 21 | } 22 | } 23 | 24 | companion object { 25 | private val log by LoggerDelegate() 26 | } 27 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/versioning/GithubVersionProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import io.github.manamiproject.modb.core.httpclient.DefaultHttpClient 4 | import io.github.manamiproject.modb.core.httpclient.HttpClient 5 | import io.github.manamiproject.modb.core.json.Json 6 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 7 | import kotlinx.coroutines.runBlocking 8 | import java.net.URI 9 | 10 | internal class GithubVersionProvider( 11 | private val uri: URI = URI("https://api.github.com/repos/manami-project/manami/releases/latest"), 12 | private val httpClient: HttpClient = DefaultHttpClient.instance, 13 | ): VersionProvider { 14 | 15 | override fun version(): SemanticVersion { 16 | return runBlocking { 17 | val response = httpClient.get(uri.toURL()) 18 | check(response.isOk()) { "Unable to check latest version, because response code wasn't 200." } 19 | 20 | val version = Json.parseJson(response.bodyAsText)!!.name 21 | SemanticVersion(version) 22 | } 23 | } 24 | } 25 | 26 | private data class GithubResponse( 27 | val name: String, 28 | ) -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/versioning/LatestVersionChecker.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | internal interface LatestVersionChecker { 4 | 5 | fun checkLatestVersion() 6 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/versioning/NewVersionAvailableEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | 5 | data class NewVersionAvailableEvent(val version: SemanticVersion) : Event -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/versioning/ResourceBasedVersionProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import io.github.manamiproject.modb.core.loadResource 4 | import kotlinx.coroutines.runBlocking 5 | 6 | object ResourceBasedVersionProvider: VersionProvider { 7 | 8 | override fun version(): SemanticVersion = SemanticVersion(runBlocking { loadResource("manami.version") }) 9 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/versioning/SemanticVersion.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | data class SemanticVersion(private val _version: String = "0.0.0") { 4 | 5 | private var trimmedVersion = _version.trim() 6 | val version 7 | get() = trimmedVersion 8 | 9 | init { 10 | require(Regex("[0-9]+\\.[0-9]+\\.[0-9]+").matches(trimmedVersion)) { "Version must be of format NUMBER.NUMBER.NUMBER" } 11 | } 12 | 13 | fun isNewerThan(other: SemanticVersion): Boolean { 14 | if (major() < other.major()) return false 15 | if (major() > other.major()) return true 16 | 17 | if (minor() < other.minor()) return false 18 | if (minor() > other.minor()) return true 19 | 20 | if (patch() < other.patch()) return false 21 | if (patch() > other.patch()) return true 22 | 23 | return false 24 | } 25 | 26 | fun isOlderThan(other: SemanticVersion): Boolean { 27 | if (this == other) return false 28 | 29 | return !isNewerThan(other) 30 | } 31 | 32 | fun major() = version.split('.')[0].toInt() 33 | 34 | fun minor() = version.split('.')[1].toInt() 35 | 36 | fun patch() = version.split('.')[2].toInt() 37 | 38 | override fun toString(): String = version 39 | } -------------------------------------------------------------------------------- /manami-app/src/main/kotlin/io/github/manamiproject/manami/app/versioning/VersionProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | internal interface VersionProvider { 4 | 5 | fun version(): SemanticVersion 6 | } 7 | 8 | -------------------------------------------------------------------------------- /manami-app/src/main/resources/config/animelist.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /manami-app/src/main/resources/manami.version: -------------------------------------------------------------------------------- 1 | 3.0.0 -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/cache/TestingAssets.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache 2 | 3 | import io.github.manamiproject.manami.app.cache.loader.CacheLoader 4 | import io.github.manamiproject.modb.core.config.* 5 | import io.github.manamiproject.modb.core.converter.AnimeConverter 6 | import io.github.manamiproject.modb.core.downloader.Downloader 7 | import io.github.manamiproject.modb.core.httpclient.HttpClient 8 | import io.github.manamiproject.modb.core.httpclient.HttpResponse 9 | import io.github.manamiproject.modb.core.httpclient.RequestBody 10 | import io.github.manamiproject.modb.core.anime.Anime 11 | import io.github.manamiproject.modb.core.anime.AnimeRaw 12 | import io.github.manamiproject.modb.core.anime.Tag 13 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 14 | import java.net.URI 15 | import java.net.URL 16 | import java.time.LocalDate 17 | import java.time.LocalDateTime 18 | import java.time.OffsetDateTime 19 | 20 | internal object TestCacheLoader : CacheLoader { 21 | override fun hostname(): Hostname = shouldNotBeInvoked() 22 | override fun loadAnime(uri: URI): Anime = shouldNotBeInvoked() 23 | } 24 | 25 | internal object MetaDataProviderTestConfig: MetaDataProviderConfig { 26 | override fun isTestContext(): Boolean = shouldNotBeInvoked() 27 | override fun hostname(): Hostname = shouldNotBeInvoked() 28 | override fun buildAnimeLink(id: AnimeId): URI = shouldNotBeInvoked() 29 | override fun buildDataDownloadLink(id: String): URI = shouldNotBeInvoked() 30 | override fun extractAnimeId(uri: URI): AnimeId = shouldNotBeInvoked() 31 | override fun fileSuffix(): FileSuffix = shouldNotBeInvoked() 32 | } 33 | 34 | internal object TestDownloader: Downloader { 35 | override suspend fun download(id: AnimeId, onDeadEntry: suspend (AnimeId) -> Unit): String = shouldNotBeInvoked() 36 | } 37 | 38 | internal object TestAnimeConverter: AnimeConverter { 39 | override suspend fun convert(rawContent: String): AnimeRaw = shouldNotBeInvoked() 40 | } 41 | 42 | internal object TestHttpClient: HttpClient { 43 | override suspend fun get(url: URL, headers: Map>): HttpResponse = shouldNotBeInvoked() 44 | override suspend fun post(url: URL, requestBody: RequestBody, headers: Map>): HttpResponse = shouldNotBeInvoked() 45 | } 46 | 47 | internal object TestAnimeCache: AnimeCache { 48 | override val availableMetaDataProvider: Set 49 | get() = shouldNotBeInvoked() 50 | override val availableTags: Set 51 | get() = shouldNotBeInvoked() 52 | override fun allEntries(metaDataProvider: Hostname): Sequence = shouldNotBeInvoked() 53 | override fun mapToMetaDataProvider(uri: URI, metaDataProvider: Hostname): Set = shouldNotBeInvoked() 54 | override fun fetch(key: URI): CacheEntry = shouldNotBeInvoked() 55 | override fun populate(key: URI, value: CacheEntry) = shouldNotBeInvoked() 56 | override fun clear() = shouldNotBeInvoked() 57 | } 58 | 59 | internal object TestConfigRegistry: ConfigRegistry { 60 | override fun boolean(key: String): Boolean = shouldNotBeInvoked() 61 | override fun double(key: String): Double = shouldNotBeInvoked() 62 | override fun int(key: String): Int = shouldNotBeInvoked() 63 | override fun list(key: String): List = shouldNotBeInvoked() 64 | override fun localDate(key: String): LocalDate = shouldNotBeInvoked() 65 | override fun localDateTime(key: String): LocalDateTime = shouldNotBeInvoked() 66 | override fun long(key: String): Long = shouldNotBeInvoked() 67 | override fun map(key: String): Map = shouldNotBeInvoked() 68 | override fun offsetDateTime(key: String): OffsetDateTime = shouldNotBeInvoked() 69 | override fun string(key: String): String = shouldNotBeInvoked() 70 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/cache/loader/SimpleCacheLoaderTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.loader 2 | 3 | import io.github.manamiproject.manami.app.cache.MetaDataProviderTestConfig 4 | import io.github.manamiproject.manami.app.cache.TestAnimeConverter 5 | import io.github.manamiproject.manami.app.cache.TestDownloader 6 | import io.github.manamiproject.modb.core.anime.Anime 7 | import io.github.manamiproject.modb.core.anime.AnimeRaw 8 | import io.github.manamiproject.modb.core.config.AnimeId 9 | import io.github.manamiproject.modb.core.config.Hostname 10 | import io.github.manamiproject.modb.core.config.MetaDataProviderConfig 11 | import io.github.manamiproject.modb.core.converter.AnimeConverter 12 | import io.github.manamiproject.modb.core.downloader.Downloader 13 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 14 | import org.assertj.core.api.Assertions.assertThat 15 | import org.junit.jupiter.api.Test 16 | import java.net.URI 17 | 18 | internal class SimpleCacheLoaderTest { 19 | 20 | @Test 21 | fun `hostname returns the hostname of the given MetaDataProviderConfig`() { 22 | // given 23 | val testConfig = object: MetaDataProviderConfig by MetaDataProviderTestConfig { 24 | override fun hostname(): Hostname = "example.org" 25 | } 26 | 27 | val simpleCacheLoader = SimpleCacheLoader( 28 | config = testConfig, 29 | downloader = TestDownloader, 30 | converter = TestAnimeConverter, 31 | ) 32 | 33 | // when 34 | val result = simpleCacheLoader.hostname() 35 | 36 | // then 37 | assertThat(result).isEqualTo(testConfig.hostname()) 38 | } 39 | 40 | @Test 41 | fun `correctly load an anime`() { 42 | // given 43 | val anime = AnimeRaw("Death Note") 44 | val expectedAnime = Anime("Death Note") 45 | 46 | val testConfig = object: MetaDataProviderConfig by MetaDataProviderTestConfig { 47 | override fun extractAnimeId(uri: URI): AnimeId = "1535" 48 | override fun hostname(): Hostname = "example.org" 49 | } 50 | 51 | val testDownloader = object: Downloader by TestDownloader { 52 | override suspend fun download(id: AnimeId, onDeadEntry: suspend (AnimeId) -> Unit): String { 53 | return if (id == "1535") "{ }" else shouldNotBeInvoked() 54 | } 55 | } 56 | 57 | val testConverter = object: AnimeConverter by TestAnimeConverter { 58 | override suspend fun convert(rawContent: String): AnimeRaw { 59 | return if (rawContent == "{ }") anime else shouldNotBeInvoked() 60 | } 61 | } 62 | 63 | val simpleCacheLoader = SimpleCacheLoader( 64 | config = testConfig, 65 | downloader = testDownloader, 66 | converter = testConverter, 67 | ) 68 | 69 | // when 70 | val result = simpleCacheLoader.loadAnime(URI("https://example.org/anime/1535")) 71 | 72 | // then 73 | assertThat(result).isEqualTo(expectedAnime) 74 | } 75 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/cache/populator/DeadEntriesCachePopulatorTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.cache.populator 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer 4 | import com.github.tomakehurst.wiremock.client.WireMock.* 5 | import io.github.manamiproject.manami.app.cache.* 6 | import io.github.manamiproject.manami.app.cache.DefaultAnimeCache 7 | import io.github.manamiproject.manami.app.cache.MetaDataProviderTestConfig 8 | import io.github.manamiproject.manami.app.cache.TestCacheLoader 9 | import io.github.manamiproject.manami.app.cache.TestConfigRegistry 10 | import io.github.manamiproject.modb.core.config.AnimeId 11 | import io.github.manamiproject.modb.core.config.ConfigRegistry 12 | import io.github.manamiproject.modb.core.config.Hostname 13 | import io.github.manamiproject.modb.core.config.MetaDataProviderConfig 14 | import io.github.manamiproject.modb.test.MockServerTestCase 15 | import io.github.manamiproject.modb.test.WireMockServerCreator 16 | import kotlinx.coroutines.runBlocking 17 | import org.assertj.core.api.Assertions.assertThat 18 | import org.junit.jupiter.api.Test 19 | import java.net.URI 20 | 21 | internal class DeadEntriesCachePopulatorTest: MockServerTestCase by WireMockServerCreator() { 22 | 23 | @Test 24 | fun `successfully populate cache with dead entries`() { 25 | // given 26 | val testCache = DefaultAnimeCache(listOf(TestCacheLoader)) 27 | 28 | val testConfig = object: MetaDataProviderConfig by MetaDataProviderTestConfig { 29 | override fun hostname(): Hostname = "example.org" 30 | override fun buildAnimeLink(id: AnimeId): URI = URI("https://${hostname()}/anime/$id") 31 | } 32 | 33 | val testConfigRegistry = object: ConfigRegistry by TestConfigRegistry { 34 | override fun boolean(key: String): Boolean = false 35 | } 36 | 37 | val cachePopulator = DeadEntriesCachePopulator( 38 | config = testConfig, 39 | url = URI("http://localhost:$port/dead-entires/all.json").toURL(), 40 | configRegistry = testConfigRegistry, 41 | ) 42 | 43 | serverInstance.stubFor( 44 | get(urlPathEqualTo("/dead-entires/all.json")).willReturn( 45 | aResponse() 46 | .withHeader("Content-Type", "application/json") 47 | .withStatus(200) 48 | .withBody(""" 49 | { 50 | "${'$'}schema": "https://raw.githubusercontent.com/manami-project/anime-offline-database/refs/tags/2025-10/anime-offline-database-minified.schema.json", 51 | "license": { 52 | "name": "GNU Affero General Public License v3.0", 53 | "url": "https://github.com/manami-project/anime-offline-database/blob/2025-10/LICENSE" 54 | }, 55 | "repository": "https://github.com/manami-project/anime-offline-database", 56 | "lastUpdate": "2020-01-01", 57 | "deadEntries": [ 58 | "12449", 59 | "65562" 60 | ] 61 | } 62 | """.trimIndent()) 63 | ) 64 | ) 65 | 66 | // when 67 | runBlocking { 68 | cachePopulator.populate(testCache) 69 | } 70 | 71 | // then 72 | assertThat(testCache.fetch(URI("https://example.org/anime/12449"))).isInstanceOf(DeadEntry::class.java) 73 | assertThat(testCache.fetch(URI("https://example.org/anime/65562"))).isInstanceOf(DeadEntry::class.java) 74 | } 75 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/commands/TestingAssets.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.commands 2 | 3 | import io.github.manamiproject.manami.app.commands.history.CommandHistory 4 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 5 | 6 | internal object TestReversibleCommand : ReversibleCommand { 7 | override fun undo() = shouldNotBeInvoked() 8 | override fun execute() = shouldNotBeInvoked() 9 | } 10 | 11 | internal object TestCommandHistory : CommandHistory { 12 | override fun push(command: ReversibleCommand) = shouldNotBeInvoked() 13 | override fun isUndoPossible(): Boolean = shouldNotBeInvoked() 14 | override fun undo() = shouldNotBeInvoked() 15 | override fun isRedoPossible(): Boolean = shouldNotBeInvoked() 16 | override fun redo() = shouldNotBeInvoked() 17 | override fun isSaved(): Boolean = shouldNotBeInvoked() 18 | override fun isUnsaved(): Boolean = shouldNotBeInvoked() 19 | override fun save() = shouldNotBeInvoked() 20 | override fun clear() = shouldNotBeInvoked() 21 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/events/TestingAssets.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.events 2 | 3 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 4 | 5 | internal object TestEventBus: EventBus { 6 | override fun subscribe(subscriber: Any) = shouldNotBeInvoked() 7 | override fun unsubscribe(subscriber: Any) = shouldNotBeInvoked() 8 | override fun post(event: Event) = shouldNotBeInvoked() 9 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/extensions/CollectionExtensionsKtTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.extensions 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Nested 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.assertThrows 10 | import java.net.URI 11 | 12 | internal class CollectionExtensionsKtTest { 13 | 14 | @Nested 15 | inner class CastToSetTests { 16 | 17 | @Test 18 | fun `successfully cast collection to a set of a specifc type`() { 19 | // given 20 | val entry1 = WatchListEntry( 21 | link = Link("https://myanimelist.net/anime/37989"), 22 | title = "Golden Kamuy 2nd Season", 23 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1180/95018t.jpg"), 24 | ) 25 | val entry2 = WatchListEntry( 26 | link = Link("https://myanimelist.net/anime/40059"), 27 | title = "Golden Kamuy 3rd Season", 28 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1763/108108t.jpg"), 29 | ) 30 | val entry3 = WatchListEntry( 31 | link = Link("https://myanimelist.net/anime/5114"), 32 | title = "Fullmetal Alchemist: Brotherhood", 33 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 34 | ) 35 | 36 | val list: List<*> = listOf( 37 | entry1, 38 | entry2, 39 | entry3, 40 | ) 41 | 42 | // when 43 | val result: Set = list.castToSet() 44 | 45 | // then 46 | assertThat(result).containsExactly(entry1, entry2, entry3) 47 | } 48 | 49 | @Test 50 | fun `throws exception if not all entries are of the same type`() { 51 | // given 52 | val entry1 = WatchListEntry( 53 | link = Link("https://myanimelist.net/anime/37989"), 54 | title = "Golden Kamuy 2nd Season", 55 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1180/95018t.jpg"), 56 | ) 57 | val entry2 = IgnoreListEntry( 58 | link = Link("https://myanimelist.net/anime/40059"), 59 | title = "Golden Kamuy 3rd Season", 60 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1763/108108t.jpg"), 61 | ) 62 | val entry3 = WatchListEntry( 63 | link = Link("https://myanimelist.net/anime/5114"), 64 | title = "Fullmetal Alchemist: Brotherhood", 65 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 66 | ) 67 | 68 | val list: List<*> = listOf( 69 | entry1, 70 | entry2, 71 | entry3, 72 | ) 73 | 74 | // when 75 | val result = assertThrows { 76 | list.castToSet() 77 | } 78 | 79 | // then 80 | assertThat(result).hasMessage("Not all items are of type [class io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry]") 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/file/CmdNewFileTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.commands.TestCommandHistory 4 | import io.github.manamiproject.manami.app.commands.history.CommandHistory 5 | import io.github.manamiproject.manami.app.state.State 6 | import io.github.manamiproject.manami.app.state.TestState 7 | import org.assertj.core.api.Assertions.assertThat 8 | import org.junit.jupiter.api.Test 9 | 10 | internal class CmdNewFileTest { 11 | 12 | @Test 13 | fun `clears command history, closes file and clears internal state`() { 14 | // given 15 | var hasCloseFileBeenCalled = false 16 | var hasClearStateBeenCalled = false 17 | var hasClearHistoryBeenCalled = false 18 | 19 | val testState = object: State by TestState { 20 | override fun closeFile() { hasCloseFileBeenCalled = true } 21 | override fun clear() { hasClearStateBeenCalled = true } 22 | } 23 | 24 | val testCommandHistory = object: CommandHistory by TestCommandHistory { 25 | override fun clear() { hasClearHistoryBeenCalled = true } 26 | } 27 | 28 | val command = CmdNewFile(testState, testCommandHistory) 29 | 30 | // when 31 | command.execute() 32 | 33 | // then 34 | assertThat(hasCloseFileBeenCalled).isTrue() 35 | assertThat(hasClearStateBeenCalled).isTrue() 36 | assertThat(hasClearHistoryBeenCalled).isTrue() 37 | } 38 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/file/ManamiVersionHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.manami.app.versioning.SemanticVersion 4 | import io.github.manamiproject.modb.test.testResource 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | import org.xml.sax.EntityResolver 8 | import org.xml.sax.InputSource 9 | import javax.xml.parsers.SAXParserFactory 10 | import kotlin.io.path.Path 11 | import kotlin.io.path.inputStream 12 | 13 | internal class ManamiVersionHandlerTest { 14 | 15 | @Test 16 | fun `successfully pase version from manami file`() { 17 | // given 18 | val versionHandler = ManamiVersionHandler() 19 | val file = testResource("file/FileParser/correctly_parse_entries.xml") 20 | 21 | val saxParser = SAXParserFactory.newInstance().apply { isValidating = true }.newSAXParser() 22 | 23 | val entityResolver = EntityResolver { _, systemId -> 24 | val fileName = Path(systemId).fileName 25 | InputSource(file.parent.resolve(fileName).toString()) 26 | } 27 | versionHandler.entityResolver = entityResolver 28 | 29 | // when 30 | saxParser.parse(file.inputStream(), versionHandler) 31 | 32 | // then 33 | assertThat(versionHandler.version).isEqualTo(SemanticVersion("3.0.0")) 34 | } 35 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/file/TestingAssets.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.file 2 | 3 | import io.github.manamiproject.modb.core.config.FileSuffix 4 | import io.github.manamiproject.modb.core.extensions.RegularFile 5 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 6 | 7 | internal object TestManamiFileParser: Parser { 8 | override fun parse(file: RegularFile): ParsedManamiFile = shouldNotBeInvoked() 9 | override fun handlesSuffix(): FileSuffix = shouldNotBeInvoked() 10 | } 11 | 12 | internal object TestFileWriter: FileWriter { 13 | override fun writeTo(file: RegularFile) = shouldNotBeInvoked() 14 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/inconsistencies/TestingAssets.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.inconsistencies 2 | 3 | import io.github.manamiproject.manami.app.inconsistencies.lists.deadentries.DeadEntriesInconsistenciesResult 4 | import io.github.manamiproject.manami.app.inconsistencies.lists.metadata.MetaDataInconsistenciesResult 5 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 6 | 7 | internal object TestInconsistencyHandler: InconsistencyHandler { 8 | override fun calculateWorkload(): Int = shouldNotBeInvoked() 9 | override fun execute(progressUpdate: (Int) -> Unit): String = shouldNotBeInvoked() 10 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = shouldNotBeInvoked() 11 | } 12 | 13 | internal object TestMetaDataInconsistencyHandler: InconsistencyHandler { 14 | override fun calculateWorkload(): Int = shouldNotBeInvoked() 15 | override fun execute(progressUpdate: (Int) -> Unit): MetaDataInconsistenciesResult = shouldNotBeInvoked() 16 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = shouldNotBeInvoked() 17 | } 18 | 19 | internal object TestDeadEntriesInconsistencyHandler: InconsistencyHandler { 20 | override fun calculateWorkload(): Int = shouldNotBeInvoked() 21 | override fun execute(progressUpdate: (Int) -> Unit): DeadEntriesInconsistenciesResult = shouldNotBeInvoked() 22 | override fun isExecutable(config: InconsistenciesSearchConfig): Boolean = shouldNotBeInvoked() 23 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/lists/LinkEntryKtTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | internal class LinkEntryKtTest { 7 | 8 | @Test 9 | fun `toString of NoLink returns an empty string`() { 10 | // when 11 | val result = NoLink.toString() 12 | 13 | // then 14 | assertThat(result).isEmpty() 15 | } 16 | 17 | @Test 18 | fun `toString of LinkEntry returns the URL as string`() { 19 | // given 20 | val link = Link("https://myanimelist.net/anime/1535") 21 | 22 | // when 23 | val result = link.toString() 24 | 25 | // then 26 | assertThat(result).isEqualTo("https://myanimelist.net/anime/1535") 27 | } 28 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/lists/animelist/CmdRemoveAnimeListEntryTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.animelist 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.state.State 5 | import io.github.manamiproject.manami.app.state.TestState 6 | import io.github.manamiproject.modb.core.anime.AnimeType.TV 7 | import org.assertj.core.api.Assertions.assertThat 8 | import org.junit.jupiter.api.Test 9 | import java.net.URI 10 | import kotlin.io.path.Path 11 | 12 | internal class CmdRemoveAnimeListEntryTest { 13 | 14 | @Test 15 | fun `remove anime list entry from anime list in state`() { 16 | // given 17 | val expectedEntry = AnimeListEntry( 18 | link = Link("https://myanimelist.net/anime/57"), 19 | title = "Beck", 20 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/11/11636t.jpg"), 21 | episodes = 26, 22 | type = TV, 23 | location = Path("some/relative/path/beck"), 24 | ) 25 | 26 | var receivedEntry: AnimeListEntry? = null 27 | val testState = object: State by TestState { 28 | override fun animeListEntrtyExists(anime: AnimeListEntry): Boolean = true 29 | override fun removeAnimeListEntry(entry: AnimeListEntry) { 30 | receivedEntry = entry 31 | } 32 | } 33 | 34 | val command = CmdRemoveAnimeListEntry( 35 | state = testState, 36 | animeListEntry = expectedEntry, 37 | ) 38 | 39 | // when 40 | val result = command.execute() 41 | 42 | // then 43 | assertThat(result).isTrue() 44 | assertThat(receivedEntry).isEqualTo(expectedEntry) 45 | } 46 | 47 | @Test 48 | fun `don't do anything of the list does not contain the entry`() { 49 | // given 50 | var receivedEntry: AnimeListEntry? = null 51 | val testState = object: State by TestState { 52 | override fun animeListEntrtyExists(anime: AnimeListEntry): Boolean = false 53 | override fun removeAnimeListEntry(entry: AnimeListEntry) { 54 | receivedEntry = entry 55 | } 56 | } 57 | 58 | val expectedEntry = AnimeListEntry( 59 | link = Link("https://myanimelist.net/anime/57"), 60 | title = "Beck", 61 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/11/11636t.jpg"), 62 | episodes = 26, 63 | type = TV, 64 | location = Path("some/relative/path/beck"), 65 | ) 66 | 67 | val command = CmdRemoveAnimeListEntry( 68 | state = testState, 69 | animeListEntry = expectedEntry, 70 | ) 71 | 72 | // when 73 | val result = command.execute() 74 | 75 | // then 76 | assertThat(result).isFalse() 77 | assertThat(receivedEntry).isNull() 78 | } 79 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/lists/ignorelist/CmdAddIgnoreListEntryTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.ignorelist 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.state.State 5 | import io.github.manamiproject.manami.app.state.TestState 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | import java.net.URI 9 | 10 | internal class CmdAddIgnoreListEntryTest { 11 | 12 | @Test 13 | fun `add the ignore list entry to the state`() { 14 | // given 15 | val savedEntries = mutableListOf() 16 | val testState = object: State by TestState { 17 | override fun ignoreList(): Set = emptySet() 18 | override fun addAllIgnoreListEntries(anime: Collection) { 19 | savedEntries.addAll(anime) 20 | } 21 | } 22 | 23 | val ignoreListEntry = IgnoreListEntry( 24 | link = Link("https://myanimelist.net/anime/5114"), 25 | title = "Fullmetal Alchemist: Brotherhood", 26 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 27 | ) 28 | 29 | val command = CmdAddIgnoreListEntry( 30 | state = testState, 31 | ignoreListEntry = ignoreListEntry, 32 | ) 33 | 34 | // when 35 | val result = command.execute() 36 | 37 | //then 38 | assertThat(result).isTrue() 39 | assertThat(savedEntries).containsExactly(ignoreListEntry) 40 | } 41 | 42 | @Test 43 | fun `don't add entry if it already exists in list`() { 44 | // given 45 | val ignoreListEntry = IgnoreListEntry( 46 | link = Link("https://myanimelist.net/anime/5114"), 47 | title = "Fullmetal Alchemist: Brotherhood", 48 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 49 | ) 50 | 51 | val savedEntries = mutableListOf() 52 | val testState = object: State by TestState { 53 | override fun ignoreList(): Set = setOf(ignoreListEntry) 54 | override fun addAllIgnoreListEntries(anime: Collection) { 55 | savedEntries.addAll(anime) 56 | } 57 | } 58 | 59 | val command = CmdAddIgnoreListEntry( 60 | state = testState, 61 | ignoreListEntry = ignoreListEntry, 62 | ) 63 | 64 | // when 65 | val result = command.execute() 66 | 67 | //then 68 | assertThat(result).isFalse() 69 | assertThat(savedEntries).isEmpty() 70 | } 71 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/lists/ignorelist/CmdRemoveIgnoreListEntryTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.ignorelist 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.state.State 5 | import io.github.manamiproject.manami.app.state.TestState 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | import java.net.URI 9 | 10 | internal class CmdRemoveIgnoreListEntryTest { 11 | 12 | @Test 13 | fun `remove ignore list entry from watch list in state`() { 14 | // given 15 | val expectedEntry = IgnoreListEntry( 16 | link = Link("https://myanimelist.net/anime/5114"), 17 | title = "Fullmetal Alchemist: Brotherhood", 18 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 19 | ) 20 | 21 | var receivedEntry: IgnoreListEntry? = null 22 | val testState = object: State by TestState { 23 | override fun ignoreList(): Set = setOf(expectedEntry) 24 | override fun removeIgnoreListEntry(entry: IgnoreListEntry) { 25 | receivedEntry = entry 26 | } 27 | } 28 | 29 | val command = CmdRemoveIgnoreListEntry( 30 | state = testState, 31 | ignoreListEntry = expectedEntry, 32 | ) 33 | 34 | // when 35 | val result = command.execute() 36 | 37 | // then 38 | assertThat(result).isTrue() 39 | assertThat(receivedEntry).isEqualTo(expectedEntry) 40 | } 41 | 42 | @Test 43 | fun `don't do anything of the list doesn not contain the entry`() { 44 | // given 45 | var receivedEntry: IgnoreListEntry? = null 46 | val testState = object: State by TestState { 47 | override fun ignoreList(): Set = emptySet() 48 | override fun removeIgnoreListEntry(entry: IgnoreListEntry) { 49 | receivedEntry = entry 50 | } 51 | } 52 | 53 | val expectedEntry = IgnoreListEntry( 54 | link = Link("https://myanimelist.net/anime/5114"), 55 | title = "Fullmetal Alchemist: Brotherhood", 56 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 57 | ) 58 | 59 | val command = CmdRemoveIgnoreListEntry( 60 | state = testState, 61 | ignoreListEntry = expectedEntry, 62 | ) 63 | 64 | // when 65 | val result = command.execute() 66 | 67 | // then 68 | assertThat(result).isFalse() 69 | assertThat(receivedEntry).isNull() 70 | } 71 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/lists/watchlist/CmdAddWatchListEntryTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.watchlist 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.state.State 5 | import io.github.manamiproject.manami.app.state.TestState 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | import java.net.URI 9 | 10 | internal class CmdAddWatchListEntryTest { 11 | 12 | @Test 13 | fun `add the watch list entry to the state`() { 14 | // given 15 | val watchListEntry = WatchListEntry( 16 | link = Link("https://myanimelist.net/anime/5114"), 17 | title = "Fullmetal Alchemist: Brotherhood", 18 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 19 | ) 20 | 21 | val savedEntries = mutableListOf() 22 | val testState = object: State by TestState { 23 | override fun watchList(): Set = emptySet() 24 | override fun addAllWatchListEntries(anime: Collection) { 25 | savedEntries.addAll(anime) 26 | } 27 | } 28 | 29 | val command = CmdAddWatchListEntry( 30 | state = testState, 31 | watchListEntry = watchListEntry, 32 | ) 33 | 34 | // when 35 | val result = command.execute() 36 | 37 | //then 38 | assertThat(result).isTrue() 39 | assertThat(savedEntries).containsExactly(watchListEntry) 40 | } 41 | 42 | @Test 43 | fun `don't add the watch list entry to the state if it already exists in list`() { 44 | // given 45 | val watchListEntry = WatchListEntry( 46 | link = Link("https://myanimelist.net/anime/5114"), 47 | title = "Fullmetal Alchemist: Brotherhood", 48 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 49 | ) 50 | 51 | val savedEntries = mutableListOf() 52 | val testState = object: State by TestState { 53 | override fun watchList(): Set = setOf(watchListEntry) 54 | override fun addAllWatchListEntries(anime: Collection) { 55 | savedEntries.addAll(anime) 56 | } 57 | } 58 | 59 | val command = CmdAddWatchListEntry( 60 | state = testState, 61 | watchListEntry = watchListEntry, 62 | ) 63 | 64 | // when 65 | val result = command.execute() 66 | 67 | //then 68 | assertThat(result).isFalse() 69 | assertThat(savedEntries).isEmpty() 70 | } 71 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/lists/watchlist/CmdRemoveWatchListEntryTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.lists.watchlist 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.state.State 5 | import io.github.manamiproject.manami.app.state.TestState 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | import java.net.URI 9 | 10 | internal class CmdRemoveWatchListEntryTest { 11 | 12 | @Test 13 | fun `remove watch list entry from watch list in state`() { 14 | // given 15 | val expectedEntry = WatchListEntry( 16 | link = Link("https://myanimelist.net/anime/5114"), 17 | title = "Fullmetal Alchemist: Brotherhood", 18 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 19 | ) 20 | 21 | var receivedEntry: WatchListEntry? = null 22 | val testState = object: State by TestState { 23 | override fun watchList(): Set = setOf(expectedEntry) 24 | override fun removeWatchListEntry(entry: WatchListEntry) { 25 | receivedEntry = entry 26 | } 27 | } 28 | 29 | val command = CmdRemoveWatchListEntry( 30 | state = testState, 31 | watchListEntry = expectedEntry, 32 | ) 33 | 34 | // when 35 | val result = command.execute() 36 | 37 | // then 38 | assertThat(result).isTrue() 39 | assertThat(receivedEntry).isEqualTo(expectedEntry) 40 | } 41 | 42 | @Test 43 | fun `don't do anything if the list does not contain the entry`() { 44 | // given 45 | var receivedEntry: WatchListEntry? = null 46 | val testState = object: State by TestState { 47 | override fun watchList(): Set = emptySet() 48 | override fun removeWatchListEntry(entry: WatchListEntry) { 49 | receivedEntry = entry 50 | } 51 | } 52 | 53 | val expectedEntry = WatchListEntry( 54 | link = Link("https://myanimelist.net/anime/5114"), 55 | title = "Fullmetal Alchemist: Brotherhood", 56 | thumbnail = URI("https://cdn.myanimelist.net/images/anime/1223/96541t.jpg"), 57 | ) 58 | 59 | val command = CmdRemoveWatchListEntry( 60 | state = testState, 61 | watchListEntry = expectedEntry, 62 | ) 63 | 64 | // when 65 | val result = command.execute() 66 | 67 | // then 68 | assertThat(result).isFalse() 69 | assertThat(receivedEntry).isNull() 70 | } 71 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/search/SearchTypeTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.search 2 | 3 | import io.github.manamiproject.manami.app.search.SearchType.AND 4 | import io.github.manamiproject.manami.app.search.SearchType.OR 5 | import io.github.manamiproject.modb.core.extensions.EMPTY 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.assertThrows 8 | import org.junit.jupiter.params.ParameterizedTest 9 | import org.junit.jupiter.params.provider.ValueSource 10 | 11 | internal class SearchTypeTest { 12 | 13 | @ParameterizedTest 14 | @ValueSource(strings = ["and", "AND", "AnD"]) 15 | fun `return AND`(value: String) { 16 | // when 17 | val result = SearchType.of(value) 18 | 19 | // then 20 | assertThat(result).isEqualTo(AND) 21 | } 22 | 23 | @ParameterizedTest 24 | @ValueSource(strings = ["or", "OR", "oR"]) 25 | fun `return OR`(value: String) { 26 | // when 27 | val result = SearchType.of(value) 28 | 29 | // then 30 | assertThat(result).isEqualTo(OR) 31 | } 32 | 33 | @ParameterizedTest 34 | @ValueSource(strings = [EMPTY, " ", "example"]) 35 | fun `throws exception if given string doesn't match any enum value`(value: String) { 36 | // when 37 | val result = assertThrows { 38 | SearchType.of(value) 39 | } 40 | 41 | // then 42 | assertThat(result).hasMessage("No value for [$value]") 43 | } 44 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/state/TestingAssets.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.state 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 5 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 6 | import io.github.manamiproject.manami.app.state.snapshot.Snapshot 7 | import io.github.manamiproject.modb.core.extensions.RegularFile 8 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 9 | 10 | internal object TestState: State { 11 | override fun setOpenedFile(file: RegularFile) = shouldNotBeInvoked() 12 | override fun openedFile(): OpenedFile = shouldNotBeInvoked() 13 | override fun closeFile() = shouldNotBeInvoked() 14 | override fun animeListEntrtyExists(anime: AnimeListEntry): Boolean = shouldNotBeInvoked() 15 | override fun animeList(): List = shouldNotBeInvoked() 16 | override fun addAllAnimeListEntries(anime: Collection) = shouldNotBeInvoked() 17 | override fun removeAnimeListEntry(entry: AnimeListEntry) = shouldNotBeInvoked() 18 | override fun watchList(): Set = shouldNotBeInvoked() 19 | override fun addAllWatchListEntries(anime: Collection) = shouldNotBeInvoked() 20 | override fun removeWatchListEntry(entry: WatchListEntry) = shouldNotBeInvoked() 21 | override fun ignoreList(): Set = shouldNotBeInvoked() 22 | override fun addAllIgnoreListEntries(anime: Collection) = shouldNotBeInvoked() 23 | override fun removeIgnoreListEntry(entry: IgnoreListEntry) = shouldNotBeInvoked() 24 | override fun createSnapshot(): Snapshot = shouldNotBeInvoked() 25 | override fun restore(snapshot: Snapshot) = shouldNotBeInvoked() 26 | override fun clear() = shouldNotBeInvoked() 27 | } 28 | 29 | internal object TestSnapshot : Snapshot { 30 | override fun animeList(): List = shouldNotBeInvoked() 31 | override fun watchList(): Set = shouldNotBeInvoked() 32 | override fun ignoreList(): Set = shouldNotBeInvoked() 33 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/versioning/DefaultLatestVersionCheckerTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import io.github.manamiproject.manami.app.events.Event 4 | import io.github.manamiproject.manami.app.events.EventBus 5 | import io.github.manamiproject.manami.app.events.TestEventBus 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.params.ParameterizedTest 9 | import org.junit.jupiter.params.provider.ValueSource 10 | 11 | internal class DefaultLatestVersionCheckerTest { 12 | 13 | @Test 14 | fun `posts event for a new version`() { 15 | // given 16 | val testCurrentVersionProvider = object: VersionProvider { 17 | override fun version(): SemanticVersion = SemanticVersion("3.0.0") 18 | } 19 | 20 | val testLatestVersionProvider = object: VersionProvider { 21 | override fun version(): SemanticVersion = SemanticVersion("3.2.2") 22 | } 23 | 24 | val events = mutableListOf() 25 | val testEventBus = object : EventBus by TestEventBus { 26 | override fun post(event: Event) { 27 | events.add(event) 28 | } 29 | } 30 | 31 | val versionChecker = DefaultLatestVersionChecker( 32 | currentVersionProvider = testCurrentVersionProvider, 33 | latestVersionProvider = testLatestVersionProvider, 34 | eventBus = testEventBus, 35 | ) 36 | 37 | // when 38 | versionChecker.checkLatestVersion() 39 | 40 | // then 41 | assertThat(events).hasSize(1) 42 | 43 | val event = events.first() 44 | assertThat(event).isInstanceOf(NewVersionAvailableEvent::class.java) 45 | 46 | assertThat((event as NewVersionAvailableEvent).version).isEqualTo(SemanticVersion("3.2.2")) 47 | } 48 | 49 | @ParameterizedTest 50 | @ValueSource(strings = ["3.1.0", "3.2.0"]) 51 | fun `don't post an event if latest version is equal to or older than current version`(versionString: String) { 52 | // given 53 | val testCurrentVersionProvider = object: VersionProvider { 54 | override fun version(): SemanticVersion = SemanticVersion("3.2.0") 55 | } 56 | 57 | val testLatestVersionProvider = object: VersionProvider { 58 | override fun version(): SemanticVersion = SemanticVersion(versionString) 59 | } 60 | 61 | val events = mutableListOf() 62 | val testEventBus = object : EventBus by TestEventBus { 63 | override fun post(event: Event) { 64 | events.add(event) 65 | } 66 | } 67 | 68 | val versionChecker = DefaultLatestVersionChecker( 69 | currentVersionProvider = testCurrentVersionProvider, 70 | latestVersionProvider = testLatestVersionProvider, 71 | eventBus = testEventBus, 72 | ) 73 | 74 | // when 75 | versionChecker.checkLatestVersion() 76 | 77 | // then 78 | assertThat(events).isEmpty() 79 | } 80 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/versioning/GithubVersionProviderTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import io.github.manamiproject.manami.app.cache.TestHttpClient 4 | import io.github.manamiproject.modb.core.extensions.EMPTY 5 | import io.github.manamiproject.modb.core.httpclient.HttpClient 6 | import io.github.manamiproject.modb.core.httpclient.HttpResponse 7 | import io.github.manamiproject.modb.test.loadTestResource 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.assertThrows 11 | import java.net.URL 12 | 13 | internal class GithubVersionProviderTest { 14 | 15 | @Test 16 | fun `correctly extract version`() { 17 | // given 18 | val testHttpClient = object: HttpClient by TestHttpClient { 19 | override suspend fun get(url: URL, headers: Map>): HttpResponse { 20 | return HttpResponse( 21 | code = 200, 22 | body = loadTestResource("versioning_tests/github_versioning_tests/latest_version.json").toByteArray(), 23 | ) 24 | } 25 | } 26 | 27 | val versionProvider = GithubVersionProvider( 28 | httpClient = testHttpClient, 29 | ) 30 | 31 | // when 32 | val result = versionProvider.version() 33 | 34 | // then 35 | assertThat(result).isEqualTo(SemanticVersion("3.12.18")) 36 | } 37 | 38 | @Test 39 | fun `throws exception if version cannot be extracted`() { 40 | // given 41 | val otherBody = """ 42 | { 43 | "name": "value" 44 | } 45 | """.trimIndent() 46 | 47 | val testHttpClient = object: HttpClient by TestHttpClient { 48 | override suspend fun get(url: URL, headers: Map>): HttpResponse { 49 | return HttpResponse( 50 | code = 200, 51 | body = otherBody.toByteArray(), 52 | ) 53 | } 54 | } 55 | 56 | val versionProvider = GithubVersionProvider( 57 | httpClient = testHttpClient, 58 | ) 59 | 60 | // when 61 | val result = assertThrows { 62 | versionProvider.version() 63 | } 64 | 65 | // then 66 | assertThat(result).hasMessage("Version must be of format NUMBER.NUMBER.NUMBER") 67 | } 68 | 69 | @Test 70 | fun `throws exception if response code is not 200`() { 71 | // given 72 | val testHttpClient = object: HttpClient by TestHttpClient { 73 | override suspend fun get(url: URL, headers: Map>): HttpResponse { 74 | return HttpResponse( 75 | code = 429, 76 | body = EMPTY.toByteArray(), 77 | ) 78 | } 79 | } 80 | 81 | val versionProvider = GithubVersionProvider( 82 | httpClient = testHttpClient, 83 | ) 84 | 85 | // when 86 | val result = assertThrows { 87 | versionProvider.version() 88 | } 89 | 90 | // then 91 | assertThat(result).hasMessage("Unable to check latest version, because response code wasn't 200.") 92 | } 93 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/versioning/ResourceBasedVersionProviderTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | internal class ResourceBasedVersionProviderTest { 7 | 8 | @Test 9 | fun `default version is 3-0-0 - the version is overwritten during release build`() { 10 | // when 11 | val result = ResourceBasedVersionProvider.version() 12 | 13 | // then 14 | assertThat(result.toString()).isEqualTo("3.0.0") 15 | } 16 | } -------------------------------------------------------------------------------- /manami-app/src/test/kotlin/io/github/manamiproject/manami/app/versioning/TestingAssets.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.app.versioning 2 | 3 | import io.github.manamiproject.modb.test.shouldNotBeInvoked 4 | 5 | internal object TestVersionProvider: VersionProvider { 6 | override fun version(): SemanticVersion = shouldNotBeInvoked() 7 | } -------------------------------------------------------------------------------- /manami-app/src/test/resources/cache_tests/loader/notify/3lack4eiR.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3lack4eiR", 3 | "type": "tv", 4 | "title": { 5 | "canonical": "Yahari Ore no Seishun Love Comedy wa Machigatteiru. Kan", 6 | "romaji": "Yahari Ore no Seishun Love Comedy wa Machigatteiru. Kan", 7 | "english": "My youth romantic comedy is wrong as I expected 3", 8 | "japanese": "やはり俺の青春ラブコメはまちがっている。第3期", 9 | "hiragana": "", 10 | "synonyms": [ 11 | "My Teen Romantic Comedy SNAFU 3", 12 | "Oregairu 3", 13 | "Yahari Ore no Seishun Love Comedy wa Machigatteiru. 3rd Season" 14 | ] 15 | }, 16 | "summary": "The third season of Oregairu. As the members of the Service Club band together to host a school prom, Hachiman, Yukino and Yui will have to put their feelings into words and truly learn to understand each other at long last… if the Service Club can even stay afloat, that is!", 17 | "status": "current", 18 | "genres": [ 19 | "Slice of Life", 20 | "Comedy", 21 | "Drama", 22 | "Romance", 23 | "School" 24 | ], 25 | "startDate": "2020-07-10", 26 | "endDate": "2020-09-25", 27 | "episodeCount": 12, 28 | "episodeLength": 24, 29 | "source": "light novel", 30 | "image": { 31 | "extension": ".jpg", 32 | "width": 426, 33 | "height": 600, 34 | "averageColor": { 35 | "hue": 0.08178633975481608, 36 | "saturation": 0.15403291070946867, 37 | "lightness": 0.5757610437170977 38 | }, 39 | "lastModified": 1593439009 40 | }, 41 | "firstChannel": "", 42 | "rating": { 43 | "overall": 7.5, 44 | "story": 5, 45 | "visuals": 5, 46 | "soundtrack": 5, 47 | "count": { 48 | "overall": 1, 49 | "story": 0, 50 | "visuals": 0, 51 | "soundtrack": 0 52 | } 53 | }, 54 | "popularity": { 55 | "watching": 12, 56 | "completed": 1, 57 | "planned": 121, 58 | "hold": 0, 59 | "dropped": 0 60 | }, 61 | "trailers": [ 62 | { 63 | "service": "Youtube", 64 | "serviceId": "VAImNGEalgw" 65 | } 66 | ], 67 | "episodes": [ 68 | "PZt0l6MGR", 69 | "EZp0_eMMRz", 70 | "EWt0leMGRm", 71 | "PZt0l6MGgZ", 72 | "EZt0l6MGgM", 73 | "PWt0_6GGg7", 74 | "EZp0_6GGgV", 75 | "EZt0_6MGgI", 76 | "EWp0_eMGRN", 77 | "PWp0_eGMgv", 78 | "EZtAleMMgO", 79 | "PWtAleMMRF" 80 | ], 81 | "mappings": [ 82 | { 83 | "service": "kitsu/anime", 84 | "serviceId": "42194" 85 | }, 86 | { 87 | "service": "myanimelist/anime", 88 | "serviceId": "39547" 89 | }, 90 | { 91 | "service": "anilist/anime", 92 | "serviceId": "108489" 93 | }, 94 | { 95 | "service": "anidb/anime", 96 | "serviceId": "14764" 97 | }, 98 | { 99 | "service": "shoboi/anime", 100 | "serviceId": "5595" 101 | } 102 | ], 103 | "posts": [ 104 | "TTi8-KrZg", 105 | "IaXMnEjWg" 106 | ], 107 | "likes": null, 108 | "created": "2019-03-28T23:11:01Z", 109 | "createdBy": "ThN0EBDmR", 110 | "edited": "2019-03-28T23:14:20Z", 111 | "editedBy": "ThN0EBDmR", 112 | "isDraft": false, 113 | "studios": [ 114 | "aj08r78kRN" 115 | ], 116 | "producers": null, 117 | "licensors": null, 118 | "links": [ 119 | { 120 | "title": "Website", 121 | "url": "http://www.tbs.co.jp/anime/oregairu/" 122 | } 123 | ] 124 | } -------------------------------------------------------------------------------- /manami-app/src/test/resources/cache_tests/loader/notify/3lack4eiR_relations.json: -------------------------------------------------------------------------------- 1 | { 2 | "animeId": "3lack4eiR", 3 | "items": [ 4 | { 5 | "animeId": "Pk0AtFmmg", 6 | "type": "prequel" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /manami-app/src/test/resources/cache_tests/populator/test-database.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/manami-project/anime-offline-database/refs/tags/2025-10/anime-offline-database.schema.json", 3 | "license": { 4 | "name": "GNU Affero General Public License v3.0", 5 | "url": "https://github.com/manami-project/anime-offline-database/blob/2025-10/LICENSE" 6 | }, 7 | "repository": "https://github.com/manami-project/anime-offline-database", 8 | "scoreRange": { 9 | "minInclusive": 1.0, 10 | "maxInclusive": 10.0 11 | }, 12 | "lastUpdate": "2020-01-01", 13 | "data": [ 14 | { 15 | "sources": [ 16 | "https://anidb.net/anime/4563", 17 | "https://anilist.co/anime/1535", 18 | "https://anime-planet.com/anime/death-note", 19 | "https://kitsu.app/anime/1376", 20 | "https://myanimelist.net/anime/1535", 21 | "https://notify.moe/anime/0-A-5Fimg" 22 | ], 23 | "title": "Death Note", 24 | "type": "TV", 25 | "episodes": 37, 26 | "status": "FINISHED", 27 | "animeSeason": { 28 | "season": "FALL", 29 | "year": 2006 30 | }, 31 | "picture": "https://cdn.myanimelist.net/images/anime/9/9453.jpg", 32 | "thumbnail": "https://cdn.myanimelist.net/images/anime/9/9453t.jpg", 33 | "duration": { 34 | "value": 1500, 35 | "unit": "SECONDS" 36 | }, 37 | "score": { 38 | "arithmeticGeometricMean": 8.631697859409492, 39 | "arithmeticMean": 8.631818181818183, 40 | "median": 8.65 41 | }, 42 | "synonyms": [ 43 | "DEATH NOTE", 44 | "DN", 45 | "Death Note - A halállista", 46 | "Death Note - Carnetul morţii", 47 | "Death Note - Zápisník smrti", 48 | "Notatnik śmierci", 49 | "Τετράδιο Θανάτου", 50 | "Бележник на Смъртта", 51 | "Тетрадь cмерти", 52 | "Үхлийн Тэмдэглэл", 53 | "دفترچه یادداشت مرگ", 54 | "كـتـاب الـموت", 55 | "डेथ नोट", 56 | "ですのーと", 57 | "デスノート", 58 | "死亡笔记", 59 | "데스노트" 60 | ], 61 | "relatedAnime": [ 62 | "https://anidb.net/anime/8146", 63 | "https://anidb.net/anime/8147", 64 | "https://anilist.co/anime/2994", 65 | "https://anime-planet.com/anime/death-note-rewrite-1-visions-of-a-god", 66 | "https://anime-planet.com/anime/death-note-rewrite-2-ls-successors", 67 | "https://kitsu.app/anime/2707", 68 | "https://myanimelist.net/anime/2994", 69 | "https://notify.moe/anime/DBBU5Kimg" 70 | ], 71 | "tags": [ 72 | "alternative present", 73 | "amnesia", 74 | "anti-hero", 75 | "asia", 76 | "based on a manga", 77 | "contemporary fantasy", 78 | "cops", 79 | "crime", 80 | "criminals", 81 | "demons", 82 | "detective", 83 | "detectives", 84 | "drama", 85 | "earth", 86 | "espionage", 87 | "gods", 88 | "japan", 89 | "male protagonist", 90 | "manga", 91 | "mind games", 92 | "mystery", 93 | "overpowered main characters", 94 | "philosophy", 95 | "plot continuity", 96 | "police", 97 | "present", 98 | "primarily adult cast", 99 | "primarily male cast", 100 | "psychological", 101 | "psychopaths", 102 | "revenge", 103 | "rivalries", 104 | "secret identity", 105 | "serial killers", 106 | "shinigami", 107 | "shounen", 108 | "supernatural", 109 | "thriller", 110 | "time skip", 111 | "tragedy", 112 | "urban", 113 | "urban fantasy", 114 | "vigilantes", 115 | "work" 116 | ] 117 | } 118 | ] 119 | } -------------------------------------------------------------------------------- /manami-app/src/test/resources/file/FileParser/animelist.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /manami-app/src/test/resources/file/FileParser/correctly_parse_entries.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /manami-app/src/test/resources/file/FileParser/url_encoded_location.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /manami-app/src/test/resources/file/FileParser/version_too_old.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /manami-app/src/test/resources/fileimport/parser/manami/LegacyManamiParser/animelist.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /manami-app/src/test/resources/fileimport/parser/manami/LegacyManamiParser/convert_type_music_to_special.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /manami-app/src/test/resources/fileimport/parser/manami/LegacyManamiParser/correctly_parse_entries.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /manami-app/src/test/resources/fileimport/parser/manami/LegacyManamiParser/version_too_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /manami-gui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | alias(libs.plugins.kotlin.jvm) 6 | alias(libs.plugins.javafxplugin) 7 | alias(libs.plugins.shadow) 8 | application 9 | } 10 | 11 | group = "io.github.manamiproject" 12 | version = project.findProperty("release.version") as String? ?: "" 13 | 14 | val githubUsername = "manami-project" 15 | 16 | repositories { 17 | mavenCentral() 18 | maven { 19 | name = "modb-app" 20 | url = uri("https://maven.pkg.github.com/$githubUsername/modb-app") 21 | credentials { 22 | username = parameter("GH_USERNAME", githubUsername) 23 | password = parameter("GH_PACKAGES_READ_TOKEN") 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | api(project(":manami-app")) 30 | api(libs.kotlin.stdlib) 31 | api(libs.modb.core) 32 | api(libs.bundles.tornadofx) 33 | 34 | setOf("win", "linux", "mac").forEach { os -> 35 | libs.bundles.javafx.get().forEach { dependency -> 36 | implementation(dependency) { 37 | artifact { 38 | classifier = os 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | javafx { 46 | version = "21" 47 | modules = listOf( 48 | "javafx.base", 49 | "javafx.controls", 50 | "javafx.graphics", 51 | "javafx.web", 52 | ) 53 | } 54 | 55 | kotlin { 56 | jvmToolchain(JavaVersion.VERSION_21.toString().toInt()) 57 | } 58 | 59 | tasks.withType().configureEach { 60 | compilerOptions { 61 | jvmTarget.set(JvmTarget.JVM_21) 62 | apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) 63 | languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) 64 | } 65 | } 66 | 67 | tasks.withType { 68 | useJUnitPlatform() 69 | reports.html.required.set(false) 70 | reports.junitXml.required.set(true) 71 | maxParallelForks = rootProject.extra["maxParallelForks"] as Int 72 | } 73 | 74 | val mainClassPath = "io.github.manamiproject.manami.gui.StartKt" 75 | application { 76 | mainClass = mainClassPath 77 | } 78 | 79 | tasks { 80 | named("shadowJar") { 81 | archiveClassifier.set("") 82 | archiveVersion.set("") 83 | manifest { 84 | attributes["Main-Class"] = mainClassPath 85 | } 86 | exclude(".gitemptydir") 87 | archiveFileName = "manami.jar" 88 | } 89 | } 90 | 91 | fun parameter(name: String, default: String = ""): String { 92 | val env = System.getenv(name) ?: "" 93 | if (env.isNotBlank()) { 94 | return env 95 | } 96 | 97 | val property = project.findProperty(name) as String? ?: "" 98 | if (property.isNotEmpty()) { 99 | return property 100 | } 101 | 102 | return default 103 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/BigPicturedAnimeEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui 2 | 3 | import io.github.manamiproject.manami.app.lists.AnimeEntry 4 | import io.github.manamiproject.manami.app.lists.Link 5 | import io.github.manamiproject.modb.core.anime.Anime 6 | import io.github.manamiproject.modb.core.anime.Title 7 | import java.net.URI 8 | 9 | data class BigPicturedAnimeEntry(override val link: Link, override val title: Title, override val thumbnail: URI): AnimeEntry { 10 | 11 | constructor(anime: Anime): this(Link(anime.sources.first()), anime.title, anime.picture) 12 | constructor(animeEntry: AnimeEntry): this(animeEntry.link.asLink(), animeEntry.title, animeEntry.thumbnail) 13 | } 14 | -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/ImageViewCache.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui 2 | 3 | import io.github.manamiproject.manami.app.cache.Cache 4 | import io.github.manamiproject.manami.app.cache.CacheEntry 5 | import io.github.manamiproject.manami.app.cache.DeadEntry 6 | import io.github.manamiproject.manami.app.cache.PresentValue 7 | import io.github.manamiproject.modb.core.logging.LoggerDelegate 8 | import javafx.scene.image.Image 9 | import java.net.URI 10 | import java.util.concurrent.ConcurrentHashMap 11 | 12 | class ImageViewCache: Cache> { 13 | 14 | private val entries = ConcurrentHashMap>() 15 | 16 | override fun fetch(key: URI): CacheEntry { 17 | return when(val entry = entries[key]) { 18 | is PresentValue, is DeadEntry -> entry 19 | null -> createEntry(key) 20 | } 21 | } 22 | 23 | override fun populate(key: URI, value: CacheEntry) { 24 | when { 25 | !entries.containsKey(key) -> entries[key] = value 26 | else -> log.warn { "Not populating cache with key [$key], because it already exists" } 27 | } 28 | } 29 | 30 | override fun clear() { 31 | log.info { "Clearing cache for thumbnails" } 32 | entries.clear() 33 | } 34 | 35 | private fun createEntry(uri: URI): CacheEntry { 36 | log.trace { "No cache hit for [$uri]. Creating a new entry." } 37 | 38 | val image = Image(uri.toString(), true) 39 | val value = PresentValue(image) 40 | populate(uri, value) 41 | 42 | return value 43 | } 44 | 45 | private companion object { 46 | private val log by LoggerDelegate() 47 | } 48 | } 49 | 50 | object GuiCaches { 51 | val imageCache = ImageViewCache() 52 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/ReadOnlyObservableValue.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui 2 | 3 | import javafx.beans.InvalidationListener 4 | import javafx.beans.value.ChangeListener 5 | import javafx.beans.value.ObservableValue 6 | 7 | data class ReadOnlyObservableValue(val obj: T) : ObservableValue { 8 | 9 | constructor(creator: () -> T): this(creator()) 10 | 11 | override fun addListener(listener: ChangeListener?) { 12 | } 13 | 14 | override fun addListener(listener: InvalidationListener?) { 15 | } 16 | 17 | override fun removeListener(listener: InvalidationListener?) { 18 | } 19 | 20 | override fun removeListener(listener: ChangeListener?) { 21 | } 22 | 23 | override fun getValue(): T = obj 24 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/SafelyExecuteActionController.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui 2 | 3 | import io.github.manamiproject.manami.gui.components.Alerts.AlertOption.* 4 | import io.github.manamiproject.manami.gui.components.Alerts.unsavedChangedAlert 5 | import io.github.manamiproject.manami.gui.components.PathChooser.showSaveAsFileDialog 6 | import tornadofx.Controller 7 | 8 | class SafelyExecuteActionController : Controller() { 9 | 10 | private val manamiAccess: ManamiAccess by inject() 11 | 12 | fun safelyExecute(action: (Boolean) -> Unit) { 13 | var option = if (manamiAccess.isUnsaved()) { 14 | unsavedChangedAlert() 15 | } else { 16 | NONE 17 | } 18 | 19 | if (option == YES) { 20 | if (manamiAccess.isOpenFileSet()) { 21 | runAsync { 22 | manamiAccess.save() 23 | } 24 | } else { 25 | val file = showSaveAsFileDialog(primaryStage) 26 | 27 | if (file != null) { 28 | manamiAccess.saveAs(file) 29 | } else { 30 | option = CANCEL 31 | } 32 | } 33 | } 34 | 35 | if (option != CANCEL) { 36 | runAsync { 37 | action.invoke(option == NO) 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/Start.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui 2 | 3 | import io.github.manamiproject.manami.app.Manami 4 | import io.github.manamiproject.manami.gui.main.MainWindowView 5 | import tornadofx.* 6 | 7 | class ManamiGui: App(MainWindowView::class) 8 | 9 | internal val manamiInstance = Manami() 10 | 11 | fun main(args: Array) { 12 | launch(args) 13 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/animelist/AnimeFormTrigger.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.animelist 2 | 3 | enum class AnimeFormTrigger { 4 | /** Add all fields manually. */ 5 | CREATE_CUSTOM, 6 | /** Add all fields automatically based on a link. */ 7 | CREATE_AUTOMATICALLY, 8 | /** Edit without */ 9 | EDIT, 10 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/animelist/ShowAnimeListTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.animelist 2 | 3 | import tornadofx.EventBus.RunOn.BackgroundThread 4 | import tornadofx.FXEvent 5 | 6 | object ShowAnimeListTabRequest : FXEvent(BackgroundThread) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/animelist/SimpleAnimeFormTriggerProperty.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.animelist 2 | 3 | import io.github.manamiproject.manami.gui.animelist.AnimeFormTrigger.CREATE_CUSTOM 4 | import io.github.manamiproject.modb.core.extensions.EMPTY 5 | import javafx.beans.property.ObjectPropertyBase 6 | 7 | class SimpleAnimeFormTriggerProperty(initialValue: AnimeFormTrigger = CREATE_CUSTOM): ObjectPropertyBase(initialValue) { 8 | 9 | override fun getBean(): Any = this 10 | override fun getName(): String = EMPTY 11 | 12 | var value: AnimeFormTrigger 13 | set(value) { super.setValue(value) } 14 | get() { return super.getValue() } 15 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/animelist/SimpleAnimeListEntryProperty.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.animelist 2 | 3 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 4 | import io.github.manamiproject.modb.core.extensions.EMPTY 5 | import javafx.beans.property.ObjectPropertyBase 6 | 7 | class SimpleAnimeListEntryProperty(initialValue: AnimeListEntry? = null): ObjectPropertyBase(initialValue) { 8 | 9 | override fun getBean(): Any = this 10 | override fun getName(): String = EMPTY 11 | 12 | var value: AnimeListEntry 13 | set(value) { super.setValue(value) } 14 | get() { return super.getValue() } 15 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/animelist/SimpleAnimeProperty.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.animelist 2 | 3 | import io.github.manamiproject.modb.core.extensions.EMPTY 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | import javafx.beans.property.ObjectPropertyBase 6 | 7 | class SimpleAnimeProperty(initialValue: Anime? = null): ObjectPropertyBase(initialValue) { 8 | 9 | override fun getBean(): Any = this 10 | override fun getName(): String = EMPTY 11 | 12 | var value: Anime 13 | set(value) { super.setValue(value) } 14 | get() { return super.getValue() } 15 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/components/Alerts.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.components 2 | 3 | import io.github.manamiproject.modb.core.anime.Title 4 | import javafx.scene.control.Alert 5 | import javafx.scene.control.Alert.AlertType.CONFIRMATION 6 | import javafx.scene.control.ButtonBar.ButtonData.* 7 | import javafx.scene.control.ButtonType 8 | 9 | object Alerts { 10 | 11 | fun unsavedChangedAlert(): AlertOption { 12 | val alertResult = Alert(CONFIRMATION).apply { 13 | title = "Unsaved changes" 14 | headerText = "Your changes will be lost if you don't save them." 15 | contentText = "Do you want to save your changes?" 16 | buttonTypes.clear() 17 | buttonTypes.addAll( 18 | ButtonType("Yes", YES), 19 | ButtonType("No", NO), 20 | ButtonType("Cancel", CANCEL_CLOSE), 21 | ) 22 | }.showAndWait().get() 23 | 24 | return when(alertResult.buttonData.typeCode) { 25 | YES.typeCode -> AlertOption.YES 26 | NO.typeCode -> AlertOption.NO 27 | CANCEL_CLOSE.typeCode -> AlertOption.CANCEL 28 | else -> throw IllegalStateException("Unknown ButtonType") 29 | } 30 | } 31 | 32 | fun removeEntry(animeTitle: Title): AlertOption { 33 | val alertResult = Alert(CONFIRMATION).apply { 34 | title = "Remove Entry" 35 | headerText = "Do you really want to remove this entry?" 36 | contentText = animeTitle 37 | buttonTypes.clear() 38 | buttonTypes.addAll( 39 | ButtonType("Yes", YES), 40 | ButtonType("No", NO), 41 | ) 42 | }.showAndWait().get() 43 | 44 | return when(alertResult.buttonData.typeCode) { 45 | YES.typeCode -> AlertOption.YES 46 | NO.typeCode -> AlertOption.NO 47 | else -> throw IllegalStateException("Unknown ButtonType") 48 | } 49 | } 50 | 51 | enum class AlertOption { 52 | YES, 53 | NO, 54 | CANCEL, 55 | NONE, 56 | } 57 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/components/ApplicationBlockedLoading.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.components 2 | 3 | import javafx.scene.Parent 4 | import tornadofx.Fragment 5 | import tornadofx.pane 6 | import tornadofx.progressindicator 7 | 8 | class ApplicationBlockedLoading : Fragment() { 9 | 10 | override val root: Parent = pane { 11 | progressindicator { 12 | progress = -1.0 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/components/PathChooser.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.components 2 | 3 | import io.github.manamiproject.modb.core.extensions.Directory 4 | import io.github.manamiproject.modb.core.extensions.RegularFile 5 | import javafx.stage.DirectoryChooser 6 | import javafx.stage.FileChooser 7 | import javafx.stage.FileChooser.ExtensionFilter 8 | import javafx.stage.Stage 9 | 10 | object PathChooser { 11 | 12 | private val XML_FILTER = ExtensionFilter("XML", "*.xml") 13 | 14 | fun showOpenFileDialog(stage: Stage): RegularFile? { 15 | val fileChooser = FileChooser().apply { 16 | title = "Select your anime list..." 17 | extensionFilters.addAll(XML_FILTER) 18 | } 19 | 20 | return fileChooser.showOpenDialog(stage).let { it?.toPath() } 21 | } 22 | 23 | fun showSaveAsFileDialog(stage: Stage): RegularFile? { 24 | val fileChooser = FileChooser().apply { 25 | title = "Save your anime list as..." 26 | extensionFilters.addAll(XML_FILTER) 27 | } 28 | 29 | return fileChooser.showSaveDialog(stage).let { it?.toPath() } 30 | } 31 | 32 | fun showBrowseForFolderDialog(stage: Stage): Directory? { 33 | val directoryChooser = DirectoryChooser().apply { 34 | title = "Browse for directory..." 35 | } 36 | 37 | return directoryChooser.showDialog(stage).let { it?.toPath() } 38 | } 39 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/components/Tiles.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.components 2 | 3 | import io.github.manamiproject.modb.core.extensions.EMPTY 4 | import javafx.beans.property.SimpleStringProperty 5 | import javafx.event.EventTarget 6 | import javafx.geometry.Pos 7 | import javafx.geometry.Pos.TOP_CENTER 8 | import javafx.scene.Group 9 | import javafx.scene.Node 10 | import javafx.scene.paint.Color 11 | import javafx.scene.shape.Rectangle 12 | import javafx.scene.text.Font 13 | import tornadofx.* 14 | 15 | data class NumberTileConfig( 16 | var title: String = EMPTY, 17 | var color: Color = Color.LIGHTGRAY, 18 | var valueProperty: SimpleStringProperty = SimpleStringProperty(EMPTY), 19 | ) 20 | 21 | inline fun EventTarget.numberTile(config: NumberTileConfig.() -> Unit): Node { 22 | val numberTileConfig = NumberTileConfig().apply(config) 23 | 24 | val tile = Rectangle(0.0, 0.0, 250.0, 150.0).apply { 25 | fill = numberTileConfig.color 26 | } 27 | 28 | val content = text { 29 | text = "0" 30 | fill = Color.WHITE 31 | font = Font.font(16.0) 32 | } 33 | 34 | val innerPane = pane { 35 | prefWidth = 250.0 36 | prefHeight = 150.0 37 | 38 | vbox { 39 | padding = insets(5) 40 | fitToParentSize() 41 | hbox { 42 | alignment = TOP_CENTER 43 | text { 44 | text = numberTileConfig.title 45 | fill = Color.WHITE 46 | } 47 | } 48 | hbox { 49 | fitToParentSize() 50 | alignment = Pos.CENTER 51 | add(content) 52 | } 53 | } 54 | } 55 | 56 | numberTileConfig.valueProperty.addListener { _, _, newValue -> 57 | content.text = newValue 58 | } 59 | 60 | val group = Group(tile, innerPane) 61 | 62 | this.add(group) 63 | 64 | return group 65 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/extensions/EventTargetExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.extensions 2 | 3 | import io.github.manamiproject.modb.core.extensions.EMPTY 4 | import io.github.manamiproject.modb.core.anime.Anime 5 | import io.github.manamiproject.modb.core.anime.AnimeStatus 6 | import io.github.manamiproject.modb.core.anime.AnimeStatus.* 7 | import javafx.application.HostServices 8 | import javafx.event.EventHandler 9 | import javafx.event.EventTarget 10 | import javafx.scene.control.Hyperlink 11 | import javafx.scene.input.MouseButton 12 | import javafx.scene.text.Font 13 | import java.net.URI 14 | 15 | data class HyperlinkConfig( 16 | var title: String = EMPTY, 17 | var uri: URI = URI(EMPTY), 18 | var font: Font = Font.font(20.0), 19 | var isDisable: Boolean = false, 20 | var animeStatus: AnimeStatus = UNKNOWN, 21 | var hostServicesInstance: HostServices? = null, 22 | ) 23 | 24 | fun EventTarget.hyperlink(config: HyperlinkConfig.() -> Unit): Hyperlink { 25 | val hyperlinkConfig = HyperlinkConfig().apply(config) 26 | val title = when (hyperlinkConfig.animeStatus) { 27 | ONGOING -> "[ongoing] ${hyperlinkConfig.title}" 28 | UPCOMING -> "[upcoming] ${hyperlinkConfig.title}" 29 | else -> hyperlinkConfig.title 30 | } 31 | 32 | return Hyperlink(title).apply { 33 | onMouseClicked = EventHandler { 34 | if (it.button == MouseButton.PRIMARY) { 35 | hyperlinkConfig.hostServicesInstance!!.showDocument(hyperlinkConfig.uri.toString()) 36 | } 37 | } 38 | font = hyperlinkConfig.font 39 | isDisable = hyperlinkConfig.isDisable 40 | } 41 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/extensions/NodeExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.extensions 2 | 3 | import javafx.scene.Node 4 | import tornadofx.runLater 5 | 6 | fun Node.focus(): Node { 7 | runLater { this.requestFocus() } 8 | return this 9 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/extensions/TabPaneExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.extensions 2 | 3 | import javafx.scene.control.Tab 4 | import javafx.scene.control.TabPane 5 | import tornadofx.runLater 6 | import tornadofx.select 7 | import tornadofx.tab 8 | 9 | /** 10 | * Adds a new [Tab] to the [TabPane] if there is currently no [Tab] with the given title and 11 | * selects it. If the [Tab] is already part of the [TabPane] it will be selected. 12 | */ 13 | fun TabPane.openTab(title: String, closeable: Boolean = true, content: Tab.() -> Unit = {}) { 14 | val isTabAlreadyOpened = this.tabs.find { it.text == title } != null 15 | 16 | if (!isTabAlreadyOpened) { 17 | runLater { 18 | this.tab(title) { 19 | isClosable = closeable 20 | also(content) 21 | } 22 | } 23 | } 24 | 25 | runLater { 26 | this.tabs.find { it.text == title }?.select() 27 | } 28 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/ignorelist/ShowIgnoreListTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.ignorelist 2 | 3 | import tornadofx.EventBus.RunOn.BackgroundThread 4 | import tornadofx.FXEvent 5 | 6 | object ShowIgnoreListTabRequest : FXEvent(BackgroundThread) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/inconsistencies/ShowInconsistenciesTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.inconsistencies 2 | 3 | import tornadofx.EventBus.RunOn.BackgroundThread 4 | import tornadofx.FXEvent 5 | 6 | object ShowInconsistenciesTabRequest : FXEvent(BackgroundThread) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/main/MenuController.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.main 2 | 3 | import io.github.manamiproject.manami.gui.ManamiAccess 4 | import io.github.manamiproject.manami.gui.SafelyExecuteActionController 5 | import io.github.manamiproject.manami.gui.components.PathChooser 6 | import io.github.manamiproject.manami.gui.search.ClearAutoCompleteSuggestionsGuiEvent 7 | import io.github.manamiproject.modb.core.extensions.RegularFile 8 | import tornadofx.Controller 9 | 10 | class MenuController : Controller() { 11 | 12 | private val manamiAccess: ManamiAccess by inject() 13 | private val safelyExecuteActionController: SafelyExecuteActionController by inject() 14 | private val quitController: QuitController by inject() 15 | 16 | fun newFile() { 17 | safelyExecuteActionController.safelyExecute { ignoreUnsavedChanged -> 18 | runAsync { 19 | manamiAccess.newFile(ignoreUnsavedChanged = ignoreUnsavedChanged) 20 | } 21 | } 22 | } 23 | 24 | fun open(file: RegularFile?) { 25 | if (file == null) { 26 | return 27 | } 28 | 29 | safelyExecuteActionController.safelyExecute { ignoreUnsavedChanged -> 30 | fire(ClearAutoCompleteSuggestionsGuiEvent) 31 | runAsync { 32 | manamiAccess.open(file = file, ignoreUnsavedChanged = ignoreUnsavedChanged) 33 | } 34 | } 35 | } 36 | 37 | fun save() { 38 | if (manamiAccess.isOpenFileSet()) { 39 | runAsync { 40 | manamiAccess.save() 41 | } 42 | } else { 43 | saveAs(PathChooser.showSaveAsFileDialog(primaryStage)) 44 | } 45 | } 46 | 47 | fun saveAs(file: RegularFile?) { 48 | if (file != null) { 49 | runAsync { 50 | manamiAccess.saveAs(file) 51 | } 52 | } 53 | } 54 | 55 | fun undo() { 56 | runAsync { 57 | manamiAccess.undo() 58 | } 59 | } 60 | 61 | fun redo() { 62 | runAsync { 63 | manamiAccess.redo() 64 | } 65 | } 66 | 67 | fun quit() { 68 | quitController.quit() 69 | } 70 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/main/QuitController.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.main 2 | 3 | import io.github.manamiproject.manami.gui.ManamiAccess 4 | import io.github.manamiproject.manami.gui.SafelyExecuteActionController 5 | import tornadofx.Controller 6 | 7 | class QuitController : Controller() { 8 | 9 | private val manamiAccess: ManamiAccess by inject() 10 | private val safelyExecuteActionController: SafelyExecuteActionController by inject() 11 | 12 | fun quit() { 13 | safelyExecuteActionController.safelyExecute { ignoreUnsavedChanged -> 14 | manamiAccess.quit(ignoreUnsavedChanged) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/migration/MigrationAlerts.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.migration 2 | 3 | import io.github.manamiproject.manami.gui.components.Alerts.AlertOption 4 | import javafx.scene.control.Alert 5 | import javafx.scene.control.ButtonBar 6 | import javafx.scene.control.ButtonType 7 | 8 | object MigrationAlerts { 9 | 10 | fun migrateEntries( 11 | numberOfEntriesAnimeList: Int, 12 | numberOfEntriesWatchList: Int, 13 | numberOfEntriesIgnoreList: Int, 14 | ): AlertOption { 15 | val totalEntries = numberOfEntriesAnimeList + numberOfEntriesWatchList + numberOfEntriesIgnoreList 16 | val alertResult = Alert(Alert.AlertType.CONFIRMATION).apply { 17 | title = "Migrate entries?" 18 | headerText = "Do you really want to migrate $totalEntries entries?" 19 | contentText = "Anime List: $numberOfEntriesAnimeList\nWatch List: $numberOfEntriesWatchList\nIgnore List: $numberOfEntriesIgnoreList" 20 | buttonTypes.clear() 21 | buttonTypes.addAll( 22 | ButtonType("Yes", ButtonBar.ButtonData.YES), 23 | ButtonType("No", ButtonBar.ButtonData.NO), 24 | ) 25 | }.showAndWait().get() 26 | 27 | return when(alertResult.buttonData.typeCode) { 28 | ButtonBar.ButtonData.YES.typeCode -> AlertOption.YES 29 | ButtonBar.ButtonData.NO.typeCode -> AlertOption.NO 30 | else -> throw IllegalStateException("Unknown ButtonType") 31 | } 32 | } 33 | 34 | fun removeUnmappedEntries( 35 | numberOfEntriesAnimeList: Int, 36 | numberOfEntriesWatchList: Int, 37 | numberOfEntriesIgnoreList: Int, 38 | ): AlertOption { 39 | val totalEntries = numberOfEntriesAnimeList + numberOfEntriesWatchList + numberOfEntriesIgnoreList 40 | val alertResult = Alert(Alert.AlertType.CONFIRMATION).apply { 41 | title = "Remove unmapped entries?" 42 | headerText = "Do you want to remove $totalEntries entries which couldn't be mapped to the new meta data provider?" 43 | contentText = "Anime List: $numberOfEntriesAnimeList\nWatch List: $numberOfEntriesWatchList\nIgnore List: $numberOfEntriesIgnoreList" 44 | buttonTypes.clear() 45 | buttonTypes.addAll( 46 | ButtonType("Yes", ButtonBar.ButtonData.YES), 47 | ButtonType("No", ButtonBar.ButtonData.NO), 48 | ) 49 | }.showAndWait().get() 50 | 51 | return when(alertResult.buttonData.typeCode) { 52 | ButtonBar.ButtonData.YES.typeCode -> AlertOption.YES 53 | ButtonBar.ButtonData.NO.typeCode -> AlertOption.NO 54 | else -> throw IllegalStateException("Unknown ButtonType") 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/migration/MigrationTableEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.migration 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.app.lists.animelist.AnimeListEntry 5 | import io.github.manamiproject.manami.app.lists.ignorelist.IgnoreListEntry 6 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 7 | import io.github.manamiproject.modb.core.anime.Title 8 | import java.net.URI 9 | 10 | internal data class MigrationTableEntry( 11 | val thumbnail: URI, 12 | val title: Title, 13 | val currentLink: Link, 14 | val alternatives: Set, 15 | val animeListEntry: AnimeListEntry? = null, 16 | val watchListEntry: WatchListEntry? = null, 17 | val ignoreListEntry: IgnoreListEntry? = null, 18 | ) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/migration/ShowMetaDataProviderMigrationViewTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.migration 2 | 3 | import tornadofx.EventBus.RunOn.BackgroundThread 4 | import tornadofx.FXEvent 5 | 6 | object ShowMetaDataProviderMigrationViewTabRequest : FXEvent(BackgroundThread) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/relatedanime/RelatedAnimeView.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.relatedanime 2 | 3 | import io.github.manamiproject.manami.app.lists.Link 4 | import io.github.manamiproject.manami.gui.* 5 | import io.github.manamiproject.manami.gui.components.animeTable 6 | import io.github.manamiproject.manami.gui.components.simpleServiceStart 7 | import io.github.manamiproject.manami.gui.events.* 8 | import javafx.beans.property.ObjectProperty 9 | import javafx.beans.property.SimpleBooleanProperty 10 | import javafx.beans.property.SimpleIntegerProperty 11 | import javafx.beans.property.SimpleObjectProperty 12 | import javafx.collections.FXCollections 13 | import javafx.collections.ObservableList 14 | import javafx.scene.layout.Priority.ALWAYS 15 | import tornadofx.* 16 | 17 | class RelatedAnimeView : View() { 18 | 19 | private val manamiAccess: ManamiAccess by inject() 20 | private val finishedTasks: SimpleIntegerProperty = SimpleIntegerProperty(0) 21 | private val tasks: SimpleIntegerProperty = SimpleIntegerProperty(0) 22 | private val isRelatedAnimeProgressIndicatorVisible = SimpleBooleanProperty(false) 23 | 24 | private val entries: ObjectProperty> = SimpleObjectProperty( 25 | FXCollections.observableArrayList() 26 | ) 27 | 28 | init { 29 | subscribe { event -> 30 | finishedTasks.set(1) 31 | tasks.set(1) 32 | entries.get().clear() 33 | event.result.forEach { entries.value.add(BigPicturedAnimeEntry(it)) } 34 | isRelatedAnimeProgressIndicatorVisible.set(false) 35 | } 36 | subscribe { event -> 37 | val uris = event.entries.map { it.link }.filterIsInstance().map { it.uri }.toSet() 38 | entries.get().removeIf { uris.contains(it.link.uri) } 39 | } 40 | subscribe { event -> 41 | val uris = event.entries.map { it.link }.map { it.uri }.toSet() 42 | entries.get().removeIf { uris.contains(it.link.uri) } 43 | } 44 | subscribe { event -> 45 | val uris = event.entries.map { it.link }.map { it.uri }.toSet() 46 | entries.get().removeIf { uris.contains(it.link.uri) } 47 | } 48 | subscribe { 49 | entries.get().clear() 50 | } 51 | } 52 | 53 | override val root = pane { 54 | 55 | vbox { 56 | vgrow = ALWAYS 57 | hgrow = ALWAYS 58 | fitToParentSize() 59 | 60 | simpleServiceStart { 61 | finishedTasksProperty.bindBidirectional(finishedTasks) 62 | numberOfTasksProperty.bindBidirectional(tasks) 63 | progressIndicatorVisibleProperty.bindBidirectional(isRelatedAnimeProgressIndicatorVisible) 64 | onStart = { 65 | entries.get().clear() 66 | manamiAccess.findRelatedAnimeForAnimeList() 67 | } 68 | } 69 | 70 | animeTable { 71 | manamiApp = manamiAccess 72 | items = entries 73 | hostServicesInstance = hostServices 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/relatedanime/ShowRelatedAnimeTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.relatedanime 2 | 3 | import tornadofx.EventBus.RunOn.BackgroundThread 4 | import tornadofx.FXEvent 5 | 6 | object ShowRelatedAnimeTabRequest : FXEvent(BackgroundThread) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/search/ClearAutoCompleteSuggestionsGuiEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.search 2 | 3 | import tornadofx.FXEvent 4 | 5 | object ClearAutoCompleteSuggestionsGuiEvent: FXEvent() -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/search/SearchBoxView.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.search 2 | 3 | import impl.org.controlsfx.autocompletion.SuggestionProvider 4 | import io.github.manamiproject.manami.gui.events.AddAnimeListEntryGuiEvent 5 | import io.github.manamiproject.manami.gui.events.AddIgnoreListEntryGuiEvent 6 | import io.github.manamiproject.manami.gui.events.AddWatchListEntryGuiEvent 7 | import io.github.manamiproject.manami.gui.ManamiAccess 8 | import io.github.manamiproject.manami.gui.search.file.ShowFileSearchTabRequest 9 | import io.github.manamiproject.modb.core.extensions.EMPTY 10 | import io.github.manamiproject.modb.core.extensions.eitherNullOrBlank 11 | import io.github.manamiproject.modb.core.anime.Title 12 | import javafx.beans.property.SimpleStringProperty 13 | import javafx.geometry.Pos.CENTER_RIGHT 14 | import tornadofx.* 15 | import tornadofx.controlsfx.bindAutoCompletion 16 | 17 | class SearchBoxView: View() { 18 | 19 | private val controller: SearchBoxController by inject() 20 | private val searchStringProperty = SimpleStringProperty() 21 | private val suggestedEntries = mutableSetOf() 22 | private val autoCompleteProvider: SuggestionProvider = SuggestionProvider.create(emptyList()) 23 | 24 | init { 25 | subscribe { event -> 26 | addTitlesToSuggestions(event.entries.map { it.title }) 27 | } 28 | subscribe { event -> 29 | addTitlesToSuggestions(event.entries.map { it.title }) 30 | } 31 | subscribe { event -> 32 | addTitlesToSuggestions(event.entries.map { it.title }) 33 | } 34 | subscribe { 35 | autoCompleteProvider.clearSuggestions() 36 | suggestedEntries.clear() 37 | } 38 | } 39 | 40 | override val root = hbox { 41 | alignment = CENTER_RIGHT 42 | spacing = 5.0 43 | 44 | textfield { 45 | promptText = "Title" 46 | textProperty().bindBidirectional(searchStringProperty) 47 | bindAutoCompletion(autoCompleteProvider) 48 | minWidth = 250.0 49 | } 50 | button("Search") { 51 | isDefaultButton = true 52 | isDisable = true 53 | searchStringProperty.onChange { e -> isDisable = e?.eitherNullOrBlank() ?: true } 54 | action { 55 | controller.search(searchStringProperty.get()) 56 | searchStringProperty.set(EMPTY) 57 | } 58 | } 59 | } 60 | 61 | private fun addTitlesToSuggestions(titles: Collection) { 62 | val suggestionsToAdd = titles.map { it }.distinct().filterNot { suggestedEntries.contains(it) } 63 | autoCompleteProvider.addPossibleSuggestions(suggestionsToAdd) 64 | suggestedEntries.addAll(suggestionsToAdd) 65 | } 66 | } 67 | 68 | class SearchBoxController: Controller() { 69 | 70 | private val manamiAccess: ManamiAccess by inject() 71 | 72 | fun search(searchString: String) { 73 | runAsync { 74 | manamiAccess.findInLists(searchString) 75 | } 76 | fire(ShowFileSearchTabRequest) 77 | } 78 | } -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/search/anime/ShowAnimeSearchTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.search.anime 2 | 3 | import tornadofx.FXEvent 4 | 5 | object ShowAnimeSearchTabRequest: FXEvent() -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/search/file/ShowFileSearchTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.search.file 2 | 3 | import tornadofx.EventBus.RunOn.BackgroundThread 4 | import tornadofx.FXEvent 5 | 6 | object ShowFileSearchTabRequest : FXEvent(BackgroundThread) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/search/season/ShowAnimeSeasonTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.search.season 2 | 3 | import tornadofx.FXEvent 4 | 5 | object ShowAnimeSeasonTabRequest: FXEvent() -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/search/similaranime/ShowSimilarAnimeSearchTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.search.similaranime 2 | 3 | import tornadofx.FXEvent 4 | 5 | object ShowSimilarAnimeSearchTabRequest: FXEvent() -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/watchlist/ShowWatchListTabRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.watchlist 2 | 3 | import tornadofx.EventBus.RunOn.BackgroundThread 4 | import tornadofx.FXEvent 5 | 6 | object ShowWatchListTabRequest : FXEvent(BackgroundThread) -------------------------------------------------------------------------------- /manami-gui/src/main/kotlin/io/github/manamiproject/manami/gui/watchlist/WatchListView.kt: -------------------------------------------------------------------------------- 1 | package io.github.manamiproject.manami.gui.watchlist 2 | 3 | import io.github.manamiproject.manami.app.lists.watchlist.WatchListEntry 4 | import io.github.manamiproject.manami.gui.events.AddWatchListEntryGuiEvent 5 | import io.github.manamiproject.manami.gui.events.AddWatchListStatusUpdateGuiEvent 6 | import io.github.manamiproject.manami.gui.ManamiAccess 7 | import io.github.manamiproject.manami.gui.events.RemoveWatchListEntryGuiEvent 8 | import io.github.manamiproject.manami.gui.components.animeTable 9 | import io.github.manamiproject.manami.gui.components.simpleAnimeAddition 10 | import javafx.beans.property.ObjectProperty 11 | import javafx.beans.property.SimpleIntegerProperty 12 | import javafx.beans.property.SimpleObjectProperty 13 | import javafx.collections.FXCollections 14 | import javafx.collections.ObservableList 15 | import javafx.scene.layout.Priority.ALWAYS 16 | import tornadofx.* 17 | 18 | class WatchListView : View() { 19 | 20 | private val manamiAccess: ManamiAccess by inject() 21 | private val finishedTasks: SimpleIntegerProperty = SimpleIntegerProperty(0) 22 | private val tasks: SimpleIntegerProperty = SimpleIntegerProperty(0) 23 | 24 | private val entries: ObjectProperty<ObservableList<WatchListEntry>> = SimpleObjectProperty( 25 | FXCollections.observableArrayList(manamiAccess.watchList()) 26 | ) 27 | 28 | init { 29 | subscribe<AddWatchListEntryGuiEvent> { event -> 30 | entries.value.addAll(event.entries) 31 | } 32 | subscribe<RemoveWatchListEntryGuiEvent> { event -> 33 | entries.value.removeAll(event.entries) 34 | } 35 | subscribe<AddWatchListStatusUpdateGuiEvent> { event -> 36 | finishedTasks.set(event.finishedTasks) 37 | tasks.set(event.tasks) 38 | } 39 | } 40 | 41 | override val root = pane { 42 | 43 | vbox { 44 | vgrow = ALWAYS 45 | hgrow = ALWAYS 46 | fitToParentSize() 47 | 48 | simpleAnimeAddition { 49 | finishedTasksProperty = finishedTasks 50 | numberOfTasksProperty = tasks 51 | onAdd = { entry -> 52 | manamiAccess.addWatchListEntry(entry) 53 | } 54 | } 55 | 56 | animeTable<WatchListEntry> { 57 | manamiApp = manamiAccess 58 | withToWatchListButton = false 59 | withHideButton = false 60 | withDeleteButton = true 61 | onDelete = { entry -> manamiAccess.removeWatchListEntry(entry) } 62 | items = entries 63 | hostServicesInstance = hostServices 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /manami-gui/src/main/resources/.gitemptydir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manami-project/manami/c3686157de21911b3795fba89bbc357d5b0eae3c/manami-gui/src/main/resources/.gitemptydir -------------------------------------------------------------------------------- /manami-gui/src/test/kotlin/.gitemptydir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manami-project/manami/c3686157de21911b3795fba89bbc357d5b0eae3c/manami-gui/src/test/kotlin/.gitemptydir -------------------------------------------------------------------------------- /manami-gui/src/test/resources/.gitemptydir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manami-project/manami/c3686157de21911b3795fba89bbc357d5b0eae3c/manami-gui/src/test/resources/.gitemptydir -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "configMigration": true, 4 | "extends": [ 5 | "config:recommended" 6 | ], 7 | "prHourlyLimit": 20, 8 | "hostRules": [ 9 | { 10 | "matchHost": "maven.pkg.github.com", 11 | "hostType": "maven", 12 | "token": "{{ secrets.RENOVATE_TOKEN }}" 13 | } 14 | ], 15 | "packageRules": [ 16 | { 17 | "description": "Automerge non-major updates", 18 | "matchUpdateTypes": [ 19 | "minor", 20 | "patch" 21 | ], 22 | "automerge": true 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // Apply the foojay-resolver plugin to allow automatic download of JDKs 3 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 4 | } 5 | 6 | rootProject.name = "manami" 7 | 8 | include("manami-gui") 9 | include("manami-app") 10 | 11 | val maxParallelForks = if (System.getenv("CI") == "true") { 12 | 2 13 | } else { 14 | (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(2) 15 | } 16 | 17 | gradle.projectsLoaded { 18 | rootProject.extra.set("maxParallelForks", maxParallelForks) 19 | } --------------------------------------------------------------------------------